@swestash/agent-lens 0.9.2 → 0.9.4

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,8 +2394,37 @@ 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
- var CLASSIFIER_VERSION = 2;
2427
+ var CLASSIFIER_VERSION = 3;
2396
2428
  var CATEGORIES = ["bugfix", "refactor", "docs", "ops", "review", "feature", "chore"];
2397
2429
  var BANDS = [
2398
2430
  { max: 22, band: "trivial" },
@@ -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 };
@@ -2438,18 +2467,27 @@ function band(score) {
2438
2467
  return "xl";
2439
2468
  }
2440
2469
  var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
2470
+ var WORD_RE = /* @__PURE__ */ new Map();
2471
+ function wordRe(phrase) {
2472
+ let re = WORD_RE.get(phrase);
2473
+ if (!re) {
2474
+ const escaped = phrase.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2475
+ re = new RegExp(`\\b${escaped}\\b`, "g");
2476
+ WORD_RE.set(phrase, re);
2477
+ }
2478
+ return re;
2479
+ }
2441
2480
  function hits(text, words) {
2442
2481
  let n = 0;
2443
2482
  for (const w of words) {
2444
- let i = text.indexOf(w);
2445
- while (i !== -1) {
2483
+ const re = wordRe(w);
2484
+ re.lastIndex = 0;
2485
+ while (re.exec(text) !== null)
2446
2486
  n++;
2447
- i = text.indexOf(w, i + w.length);
2448
- }
2449
2487
  }
2450
2488
  return n;
2451
2489
  }
2452
- function scoreCategories(s, text) {
2490
+ function scoreCategories(s, text, files) {
2453
2491
  const t = text.toLowerCase();
2454
2492
  const editN = s.toolCounts["Edit"] ?? 0;
2455
2493
  const writeN = s.toolCounts["Write"] ?? 0;
@@ -2467,11 +2505,11 @@ function scoreCategories(s, text) {
2467
2505
  scores.bugfix += hits(t, ["bug", "fix", "broken", "crash", "regression", "traceback", "stack trace", "failing", "error", "exception", "not working"]) * 1;
2468
2506
  scores.refactor += hits(t, ["refactor", "clean up", "cleanup", "rename", "extract", "simplify", "dead code", "tidy", "reorganize", "restructure"]) * 1;
2469
2507
  scores.docs += hits(t, ["readme", "document", "documentation", "changelog", "docstring", "comment", "write docs", "usage guide"]) * 1;
2470
- scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci ", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
2508
+ scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
2471
2509
  scores.review += hits(t, ["review", "audit", "inspect", "look over", "feedback on"]) * 1;
2472
- 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;
2510
+ scores.feature += hits(t, ["add", "implement", "build", "create", "feature", "support for", "new feature"]) * 0.8;
2511
+ const docFiles = s.loc.files > 0 ? countMatch(files, DOC_EXT) : 0;
2512
+ const opsFiles = s.loc.files > 0 ? countMatch(files, OPS_FILE) : 0;
2475
2513
  if (s.loc.files > 0) {
2476
2514
  scores.docs += 3 * (docFiles / s.loc.files);
2477
2515
  scores.ops += 3 * (opsFiles / s.loc.files);
@@ -2489,8 +2527,7 @@ function scoreCategories(s, text) {
2489
2527
  scores[writes > 0 ? "feature" : "chore"] += 0.01;
2490
2528
  return scores;
2491
2529
  }
2492
- function countMatch(s, re) {
2493
- const files = s._files ?? [];
2530
+ function countMatch(files, re) {
2494
2531
  let n = 0;
2495
2532
  for (const f of files)
2496
2533
  if (re.test(f))
@@ -2514,15 +2551,8 @@ function complexity(s) {
2514
2551
  }
2515
2552
  function classify(db, dirty) {
2516
2553
  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
- }
2554
+ if (dirty != null)
2555
+ createDirtySet(db, "_dirty", dirty);
2526
2556
  const wSess = incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "";
2527
2557
  const aSess = incremental ? " AND session_id IN (SELECT id FROM _dirty)" : "";
2528
2558
  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 +2617,9 @@ function classify(db, dirty) {
2587
2617
  cache_read_tokens: tok.cacheRead,
2588
2618
  duration_ms: sess.duration_ms ?? 0,
2589
2619
  subagent_count: subagentCount,
2590
- is_sidechain: sess.is_sidechain,
2591
- _files: filesArr
2620
+ is_sidechain: sess.is_sidechain
2592
2621
  };
2593
- const catScores = scoreCategories(signals, promptText.get(sess.id) ?? "");
2622
+ const catScores = scoreCategories(signals, promptText.get(sess.id) ?? "", filesArr);
2594
2623
  let category = "chore";
2595
2624
  let bestScore = -1;
2596
2625
  for (const c of CATEGORIES) {
@@ -2670,9 +2699,12 @@ var SQL_CLIENT = /\b(psql|pgcli|mysql|mysqladmin|mariadb|sqlite3?|sqlcmd|usql|mo
2670
2699
  var stripComments = (c) => c.replace(/(^|\s)#[^\n]*/g, "$1");
2671
2700
  var neutralizeEcho = (c) => c.replace(/((?:^|[;&|\n(])\s*)(?:echo|printf)\b(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^;&|\n])*/gi, "$1echo");
2672
2701
  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
2702
  var blankQuoted = (c) => c.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'[^']*'/g, "''");
2675
- var bareOf = (c) => blankQuoted(codeOf(c));
2703
+ var codeOf = (c) => neutralizeEcho(stripComments(blankHeredoc(c)));
2704
+ function commandViews(raw) {
2705
+ const code = codeOf(raw);
2706
+ return { code, bare: blankQuoted(code) };
2707
+ }
2676
2708
  function execGeneratedScript(cmd) {
2677
2709
  const targets = /* @__PURE__ */ new Set();
2678
2710
  for (const m of cmd.matchAll(/(?:>>?|\btee\b\s+(?:-a\s+)?)\s*["']?([A-Za-z0-9._/~-]+)/g)) {
@@ -2741,6 +2773,19 @@ function netTargetScope(rawCommand) {
2741
2773
  }
2742
2774
  return scope;
2743
2775
  }
2776
+ function simpleCommandRule(spec) {
2777
+ const { match, tools = ["Bash"], ...meta } = spec;
2778
+ return {
2779
+ ...meta,
2780
+ tools,
2781
+ test(ctx) {
2782
+ const c = ctx.commandBare;
2783
+ if (!c || !match.test(c))
2784
+ return null;
2785
+ return { evidence: evidence(ctx.command) };
2786
+ }
2787
+ };
2788
+ }
2744
2789
  var RULES = [
2745
2790
  // --- Destructive / data-loss (OWASP ASI02 Tool Misuse) -------------------
2746
2791
  {
@@ -2769,21 +2814,15 @@ var RULES = [
2769
2814
  return { evidence: evidence(ctx.command), severity, mods: { dangerous_target: dangerousTarget, temp_target: tempOnly } };
2770
2815
  }
2771
2816
  },
2772
- {
2817
+ simpleCommandRule({
2773
2818
  id: "destructive.git_reset_hard",
2774
2819
  category: "destructive",
2775
2820
  framework_ref: "OWASP ASI02",
2776
2821
  title: "Discards local changes (git reset --hard)",
2777
- tools: ["Bash"],
2778
2822
  baseSeverity: "low",
2779
2823
  // 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
- },
2824
+ match: /\bgit\s+reset\s+--hard\b/i
2825
+ }),
2787
2826
  {
2788
2827
  id: "destructive.git_force_push",
2789
2828
  category: "destructive",
@@ -2817,20 +2856,14 @@ var RULES = [
2817
2856
  return null;
2818
2857
  }
2819
2858
  },
2820
- {
2859
+ simpleCommandRule({
2821
2860
  id: "destructive.disk_wipe",
2822
2861
  category: "destructive",
2823
2862
  framework_ref: "OWASP ASI02",
2824
2863
  title: "Low-level disk write / wipe (dd, mkfs, shred)",
2825
- tools: ["Bash"],
2826
2864
  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
- },
2865
+ match: /(\bdd\s+if=|\bmkfs(\.\w+)?\b|\bshred\b)/i
2866
+ }),
2834
2867
  {
2835
2868
  id: "destructive.overwrite_critical",
2836
2869
  category: "destructive",
@@ -2905,7 +2938,7 @@ var RULES = [
2905
2938
  return null;
2906
2939
  if (!/(\||<)\s*(sudo\s+)?(curl|wget|nc|ncat|netcat|ssh|scp)\b/i.test(c))
2907
2940
  return null;
2908
- if (!/\b(cat|tar|zip|gzip|base64|env|printenv|cat\s|<)\b/i.test(c))
2941
+ if (!/(?:\b(?:cat|tar|zip|gzip|base64|env|printenv)\b|<)/i.test(c))
2909
2942
  return null;
2910
2943
  return { evidence: evidence(ctx.command) };
2911
2944
  }
@@ -2940,37 +2973,24 @@ var RULES = [
2940
2973
  };
2941
2974
  }
2942
2975
  },
2943
- {
2976
+ simpleCommandRule({
2944
2977
  id: "exfil.reverse_shell",
2945
2978
  category: "exfiltration",
2946
2979
  framework_ref: "MITRE ATLAS AML.T0086",
2947
2980
  title: "Netcat / reverse-shell style connection",
2948
- tools: ["Bash"],
2949
2981
  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
- },
2982
+ match: /\b(nc|ncat|netcat)\b[^|;&\n]*(-e\b|-c\b|\d{2,5})|\/dev\/tcp\/|bash\s+-i\b/i
2983
+ }),
2959
2984
  // --- Privilege escalation / guardrail bypass (OWASP LLM06 Excessive Agency)
2960
- {
2985
+ simpleCommandRule({
2961
2986
  id: "privilege.skip_permissions",
2962
2987
  category: "privilege-bypass",
2963
2988
  framework_ref: "OWASP LLM06",
2964
2989
  title: "Bypasses the permission guardrail",
2965
2990
  tools: "*",
2966
2991
  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
- },
2992
+ match: /--dangerously-skip-permissions\b|--yolo\b|--dangerously-skip\b/i
2993
+ }),
2974
2994
  {
2975
2995
  id: "privilege.curl_pipe_shell",
2976
2996
  category: "privilege-bypass",
@@ -3020,21 +3040,17 @@ var RULES = [
3020
3040
  return { evidence: evidence(ctx.command), severity: sudo ? "critical" : "high", mods: { decoder: decoderName, interpreter: interp } };
3021
3041
  }
3022
3042
  },
3023
- {
3043
+ simpleCommandRule({
3024
3044
  id: "privilege.sudo",
3025
3045
  category: "privilege-bypass",
3026
3046
  framework_ref: "OWASP LLM06",
3027
3047
  title: "Runs a command as root (sudo)",
3028
- tools: ["Bash"],
3029
3048
  baseSeverity: "high",
3030
3049
  // 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
- },
3050
+ // sudo must be in *command position* — start of the command or right after a separator (; | && ( ).
3051
+ // This ignores `sudo` as an argument (e.g. `apt install sudo`) and, via codeOf, inside echo/comments.
3052
+ match: /(?:^|[\n;|&(])\s*sudo\b/i
3053
+ }),
3038
3054
  {
3039
3055
  id: "privilege.exec_generated_script",
3040
3056
  category: "privilege-bypass",
@@ -3053,36 +3069,22 @@ var RULES = [
3053
3069
  return { evidence: evidence(raw), severity: critical ? "critical" : "high", mods: { script_file: file } };
3054
3070
  }
3055
3071
  },
3056
- {
3072
+ simpleCommandRule({
3057
3073
  id: "privilege.chmod_777",
3058
3074
  category: "privilege-bypass",
3059
3075
  framework_ref: "OWASP LLM06",
3060
3076
  title: "Overly permissive permissions (chmod 777)",
3061
- tools: ["Bash"],
3062
3077
  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
- {
3078
+ match: /\bchmod\s+(-[a-zA-Z]+\s+)*0?777\b/i
3079
+ }),
3080
+ simpleCommandRule({
3071
3081
  id: "privilege.persistence",
3072
3082
  category: "privilege-bypass",
3073
3083
  framework_ref: "OWASP LLM06",
3074
3084
  title: "Installs a persistence mechanism",
3075
- tools: ["Bash"],
3076
3085
  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
- },
3086
+ match: /\bcrontab\s+-|\bsystemctl\s+enable\b|\blaunchctl\s+load\b|>>\s*~\/\.(bashrc|zshrc|profile|bash_profile)\b|\/etc\/(cron|systemd|rc\.local)/i
3087
+ }),
3086
3088
  {
3087
3089
  id: "privilege.write_outside_project",
3088
3090
  category: "privilege-bypass",
@@ -3110,29 +3112,18 @@ function appliesTo(rule, toolName) {
3110
3112
  return rule.tools === "*" || rule.tools.includes(toolName);
3111
3113
  }
3112
3114
  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
- }
3115
+ const input = parseStored(row.input_json, "detect: tool_calls.input_json");
3119
3116
  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;
3117
+ const views = command != null ? commandViews(command) : null;
3118
+ const commandCode = views?.code ?? null;
3119
+ const commandBare = views?.bare ?? null;
3122
3120
  const filePath = typeof input?.file_path === "string" ? input.file_path : null;
3123
3121
  return { toolName: row.tool_name, command, commandCode, commandBare, filePath, input, resultSummary: row.result_summary, status: row.status, projectPath, ownedConfigDirs };
3124
3122
  }
3125
3123
  function detect(db, dirty) {
3126
3124
  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
- }
3125
+ if (dirty != null)
3126
+ createDirtySet(db, "_dirty_sec", dirty);
3136
3127
  const scope = incremental ? " WHERE tc.session_id IN (SELECT id FROM _dirty_sec)" : "";
3137
3128
  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
3129
  const projPath = /* @__PURE__ */ new Map();
@@ -3201,15 +3192,8 @@ function detect(db, dirty) {
3201
3192
  function classifyErrors(db, dirty) {
3202
3193
  const incremental = dirty != null;
3203
3194
  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
- }
3195
+ if (dirty != null)
3196
+ createDirtySet(db, "_dirty_err", dirty);
3213
3197
  const rows = db.prepare(`SELECT id, result_summary FROM tool_calls WHERE status = 'error'${scope}`).all();
3214
3198
  const update = db.prepare("UPDATE tool_calls SET error_type = ? WHERE id = ?");
3215
3199
  const tx = db.transaction(() => {
@@ -3459,14 +3443,8 @@ function pruneExcluded(db, excludedPaths) {
3459
3443
  const projIds = projects.filter((p) => excludedPaths.some((e) => p.path === e || p.path.startsWith(e + "/"))).map((p) => p.id);
3460
3444
  if (!projIds.length)
3461
3445
  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
3446
  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
- })();
3447
+ createDirtySet(db, "_prune", seed.map((s) => s.id));
3470
3448
  const expand = db.prepare("INSERT OR IGNORE INTO _prune (id) SELECT id FROM sessions WHERE parent_session_id IN (SELECT id FROM _prune)");
3471
3449
  const cnt = db.prepare("SELECT COUNT(*) n FROM _prune");
3472
3450
  for (let prev = -1; ; ) {
@@ -3543,54 +3521,36 @@ function buildTurns(sessionId, rows) {
3543
3521
  t.duration_ms = durationMs(t.started_at, t.ended_at);
3544
3522
  return { turns, eventTurn };
3545
3523
  }
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
- }
3524
+ function expandDirtyScope(db, dirty) {
3525
+ createDirtySet(db, "_dirty", dirty);
3526
+ const expand = db.prepare(`
3527
+ INSERT OR IGNORE INTO _dirty (id)
3528
+ SELECT spawned_session_id FROM tool_calls
3529
+ WHERE session_id IN (SELECT id FROM _dirty) AND spawned_session_id IS NOT NULL
3530
+ UNION
3531
+ SELECT session_id FROM tool_calls
3532
+ WHERE spawned_session_id IN (SELECT id FROM _dirty)
3533
+ UNION
3534
+ -- structural (path-based) workflow linkage: a dirty parent pulls in its subagent children,
3535
+ -- and a dirty child pulls in its structural parent, so the fallback link stays correct when
3536
+ -- parent and child transcripts arrive in different runs (ADR-010).
3537
+ SELECT id FROM sessions
3538
+ WHERE spawn_parent_id IN (SELECT id FROM _dirty)
3539
+ UNION
3540
+ SELECT spawn_parent_id FROM sessions
3541
+ WHERE id IN (SELECT id FROM _dirty) AND spawn_parent_id IS NOT NULL
3542
+ `);
3543
+ const count = db.prepare("SELECT COUNT(*) n FROM _dirty");
3544
+ let prev = -1;
3545
+ for (; ; ) {
3546
+ const n = count.get().n;
3547
+ if (n === prev)
3548
+ break;
3549
+ prev = n;
3550
+ expand.run();
3586
3551
  }
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();
3552
+ }
3553
+ function rebuildTurns(db, scope, sessionIds) {
3594
3554
  const selEvents = db.prepare("SELECT uuid, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?");
3595
3555
  const insTurn = db.prepare(`INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)
3596
3556
  VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`);
@@ -3606,123 +3566,152 @@ function rebuildDerived(db, dirty) {
3606
3566
  }
3607
3567
  });
3608
3568
  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)
3569
+ db.exec(`UPDATE token_usage SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = token_usage.event_uuid)${scope.bySession}`);
3570
+ db.exec(`UPDATE tool_calls SET turn_id = (SELECT turn_id FROM events WHERE events.uuid = tool_calls.event_uuid)${scope.bySession}`);
3571
+ }
3572
+ function linkSkillVersions(db, sessionIds) {
3573
+ const selEventsOrdered = db.prepare("SELECT uuid, type, is_meta, timestamp, text FROM events WHERE session_id = ?");
3574
+ const selSkillCalls = db.prepare("SELECT id, event_uuid, skill_name, input_json FROM tool_calls WHERE session_id = ? AND tool_name = 'Skill'");
3575
+ const upsertSkill = db.prepare(`INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)
3576
+ VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @ts, @ts)
3577
+ ON CONFLICT(id) DO UPDATE SET
3578
+ last_seen = MAX(last_seen, excluded.last_seen),
3579
+ first_seen = MIN(first_seen, excluded.first_seen),
3580
+ base_dir = excluded.base_dir`);
3581
+ const linkCall = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE id = ?");
3582
+ const linkTx = db.transaction(() => {
3583
+ for (const { id: sid } of sessionIds) {
3584
+ const calls = selSkillCalls.all(sid);
3585
+ if (!calls.length)
3586
+ continue;
3587
+ const events = selEventsOrdered.all(sid).sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || x.uuid.localeCompare(y.uuid));
3588
+ const callByEvent = /* @__PURE__ */ new Map();
3589
+ for (const c of calls) {
3590
+ if (!c.event_uuid)
3625
3591
  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);
3592
+ const arr = callByEvent.get(c.event_uuid) ?? [];
3593
+ arr.push(c);
3594
+ callByEvent.set(c.event_uuid, arr);
3595
+ }
3596
+ const pending = /* @__PURE__ */ new Map();
3597
+ for (const e of events) {
3598
+ const here = callByEvent.get(e.uuid);
3599
+ if (here) {
3600
+ for (const c2 of here) {
3601
+ if (!c2.skill_name)
3602
+ continue;
3603
+ const key = skillNameKey(c2.skill_name);
3604
+ let args = null;
3605
+ try {
3606
+ args = c2.input_json ? JSON.parse(c2.input_json).args ?? null : null;
3607
+ if (typeof args === "string")
3608
+ args = args.trim() || null;
3609
+ else if (args != null)
3610
+ args = JSON.stringify(args);
3611
+ } catch {
3655
3612
  }
3613
+ const arr = pending.get(key) ?? [];
3614
+ arr.push({ id: c2.id, args });
3615
+ pending.set(key, arr);
3656
3616
  }
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
3617
  }
3618
+ if (e.is_meta !== 1 || e.type !== "user" || !e.text)
3619
+ continue;
3620
+ const inj = parseSkillInjection(e.text);
3621
+ if (!inj)
3622
+ continue;
3623
+ const queue = pending.get(inj.nameKey);
3624
+ if (!queue || !queue.length)
3625
+ continue;
3626
+ let idx = inj.args != null ? queue.findIndex((q) => q.args === inj.args) : -1;
3627
+ if (idx < 0)
3628
+ idx = 0;
3629
+ const [match] = queue.splice(idx, 1);
3630
+ const c = calls.find((x) => x.id === match.id);
3631
+ const name = c.skill_name;
3632
+ const vid = skillVersionId(name, inj.body);
3633
+ upsertSkill.run({
3634
+ id: vid,
3635
+ name,
3636
+ base_dir: inj.baseDir,
3637
+ body: inj.body,
3638
+ summary: skillSummary(inj.body),
3639
+ body_bytes: Buffer.byteLength(inj.body, "utf8"),
3640
+ ts: e.timestamp
3641
+ });
3642
+ linkCall.run(vid, match.id);
3683
3643
  }
3684
- });
3685
- linkTx();
3686
- }
3644
+ }
3645
+ });
3646
+ linkTx();
3647
+ }
3648
+ function linkParents(db, scope) {
3687
3649
  db.exec(`
3688
3650
  UPDATE sessions SET
3689
3651
  parent_session_id = (SELECT tc.session_id FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id),
3690
3652
  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}
3653
+ WHERE EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.spawned_session_id = sessions.id)${scope.andSelfId}
3692
3654
  `);
3693
3655
  db.exec(`
3694
3656
  UPDATE sessions SET
3695
3657
  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
3658
  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
3659
  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}
3660
+ AND EXISTS (SELECT 1 FROM tool_calls tc WHERE tc.workflow_run_id = sessions.workflow_run_id)${scope.andSelfId}
3699
3661
  `);
3700
3662
  db.exec(`
3701
3663
  UPDATE sessions SET parent_session_id = spawn_parent_id
3702
3664
  WHERE parent_session_id IS NULL
3703
3665
  AND spawn_parent_id IS NOT NULL
3704
- AND spawn_parent_id IN (SELECT id FROM sessions)${andSelfId}
3666
+ AND spawn_parent_id IN (SELECT id FROM sessions)${scope.andSelfId}
3705
3667
  `);
3668
+ }
3669
+ function recomputeSessionAggregates(db, scope) {
3706
3670
  db.exec(`
3707
3671
  UPDATE sessions SET
3708
3672
  event_count = (SELECT COUNT(*) FROM events e WHERE e.session_id = sessions.id),
3709
3673
  started_at = (SELECT MIN(timestamp) FROM events e WHERE e.session_id = sessions.id),
3710
3674
  ended_at = (SELECT MAX(timestamp) FROM events e WHERE e.session_id = sessions.id),
3711
3675
  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}
3676
+ 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
3677
  `);
3714
3678
  db.exec(`
3715
3679
  UPDATE sessions SET duration_ms =
3716
3680
  CAST((julianday(ended_at) - julianday(started_at)) * 86400000 AS INTEGER)
3717
- WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${andId}
3681
+ WHERE started_at IS NOT NULL AND ended_at IS NOT NULL${scope.andId}
3718
3682
  `);
3719
- db.exec(`DELETE FROM sessions WHERE event_count = 0${andId}`);
3683
+ }
3684
+ function pruneOrphans(db, scope, incremental) {
3685
+ db.exec(`DELETE FROM sessions WHERE event_count = 0${scope.andId}`);
3720
3686
  db.exec("DELETE FROM classifications WHERE scope = 'session' AND target_id NOT IN (SELECT id FROM sessions)");
3721
3687
  db.exec("DELETE FROM findings WHERE session_id NOT IN (SELECT id FROM sessions)");
3722
3688
  db.exec("DELETE FROM file_changes WHERE session_id NOT IN (SELECT id FROM sessions)");
3723
3689
  if (!incremental) {
3724
3690
  db.exec("DELETE FROM skills WHERE id NOT IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)");
3725
3691
  }
3692
+ }
3693
+ function rebuildDerived(db, dirty) {
3694
+ const incremental = dirty != null;
3695
+ const scope = {
3696
+ bySession: incremental ? " WHERE session_id IN (SELECT id FROM _dirty)" : "",
3697
+ byId: incremental ? " WHERE id IN (SELECT id FROM _dirty)" : "",
3698
+ andId: incremental ? " AND id IN (SELECT id FROM _dirty)" : "",
3699
+ andSelfId: incremental ? " AND sessions.id IN (SELECT id FROM _dirty)" : ""
3700
+ };
3701
+ if (dirty != null)
3702
+ expandDirtyScope(db, dirty);
3703
+ db.exec(`UPDATE events SET turn_id = NULL${scope.bySession}`);
3704
+ db.exec(`UPDATE token_usage SET turn_id = NULL${scope.bySession}`);
3705
+ db.exec(`UPDATE tool_calls SET turn_id = NULL${scope.bySession}`);
3706
+ db.exec(`UPDATE tool_calls SET skill_id = NULL${scope.bySession}`);
3707
+ db.exec(`UPDATE sessions SET parent_turn_id = NULL${scope.byId}`);
3708
+ db.exec(`DELETE FROM turns${scope.bySession}`);
3709
+ const sessionIds = db.prepare(`SELECT id FROM sessions${scope.byId}`).all();
3710
+ rebuildTurns(db, scope, sessionIds);
3711
+ linkSkillVersions(db, sessionIds);
3712
+ linkParents(db, scope);
3713
+ recomputeSessionAggregates(db, scope);
3714
+ pruneOrphans(db, scope, incremental);
3726
3715
  if (incremental) {
3727
3716
  const expanded = new Set(db.prepare("SELECT id FROM _dirty").all().map((r) => r.id));
3728
3717
  db.exec("DROP TABLE IF EXISTS _dirty");
@@ -3869,8 +3858,11 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
3869
3858
  }
3870
3859
 
3871
3860
  // ../ingest/dist/workflows.js
3861
+ import { basename as basename3 } from "path";
3862
+
3863
+ // ../ingest/dist/sidecar.js
3872
3864
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3873
- import { basename as basename3, join as join7 } from "path";
3865
+ import { join as join7 } from "path";
3874
3866
 
3875
3867
  // ../ingest/dist/redact.js
3876
3868
  import { createHash as createHash6, randomBytes } from "crypto";
@@ -3926,11 +3918,18 @@ function* streamLines(path3) {
3926
3918
  }
3927
3919
  }
3928
3920
 
3929
- // ../ingest/dist/workflows.js
3930
- function newWorkflowStats() {
3921
+ // ../ingest/dist/sidecar.js
3922
+ function newSidecarStats() {
3931
3923
  return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
3932
3924
  }
3933
- function walkSidecars(dir, inWorkflows, out) {
3925
+ var intOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
3926
+ var strOrNull = (v) => typeof v === "string" && v ? v : null;
3927
+ function sessionBefore(path3, subdir) {
3928
+ const parts = path3.split("/");
3929
+ const i = parts.indexOf(subdir);
3930
+ return i > 0 ? parts[i - 1] : null;
3931
+ }
3932
+ function walk(dir, subdir, inSubdir, matchName, out) {
3934
3933
  let entries;
3935
3934
  try {
3936
3935
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -3940,47 +3939,24 @@ function walkSidecars(dir, inWorkflows, out) {
3940
3939
  for (const e of entries) {
3941
3940
  const p = join7(dir, e.name);
3942
3941
  if (e.isDirectory())
3943
- walkSidecars(p, inWorkflows || e.name === "workflows", out);
3944
- else if (e.isFile() && inWorkflows && /^wf_.+\.json$/.test(e.name))
3942
+ walk(p, subdir, inSubdir || e.name === subdir, matchName, out);
3943
+ else if (e.isFile() && inSubdir && matchName(e.name))
3945
3944
  out.push(p);
3946
3945
  }
3947
3946
  }
3948
- function discoverSidecars(sourceArchiveDir) {
3947
+ function discover(sourceArchiveDir, subdir, matchName) {
3949
3948
  const out = [];
3950
- walkSidecars(join7(sourceArchiveDir, "projects"), false, out);
3949
+ walk(join7(sourceArchiveDir, "projects"), subdir, false, matchName, out);
3951
3950
  try {
3952
3951
  for (const ts of readdirSync3(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
3953
3952
  if (ts.isDirectory())
3954
- walkSidecars(join7(sourceArchiveDir, ts.name, "projects"), false, out);
3953
+ walk(join7(sourceArchiveDir, ts.name, "projects"), subdir, false, matchName, out);
3955
3954
  } catch {
3956
3955
  }
3957
3956
  return out;
3958
3957
  }
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);
3958
+ function ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, cfg) {
3959
+ let paths = discover(sourceArchiveDir, cfg.subdir, cfg.matchName);
3984
3960
  if (excludedDirs.length)
3985
3961
  paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
3986
3962
  paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
@@ -3988,23 +3964,9 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
3988
3964
  const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
3989
3965
  VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
3990
3966
  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
3967
  for (const path3 of paths) {
3968
+ if (cfg.include && !cfg.include(path3))
3969
+ continue;
4008
3970
  stats.files++;
4009
3971
  const st = statSync2(path3);
4010
3972
  const mtimeMs = Math.trunc(st.mtimeMs);
@@ -4026,232 +3988,160 @@ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now
4026
3988
  stats.skipped++;
4027
3989
  continue;
4028
3990
  }
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") {
3991
+ if (cfg.handle(path3, buf, st.size)) {
3992
+ setState.run({ file_path: path3, size: st.size, mtime_ms: mtimeMs, sha256: hash2, ingested_at: now2 });
3993
+ stats.upserted++;
3994
+ } else {
4037
3995
  stats.malformed++;
4038
- continue;
4039
3996
  }
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
3997
  }
4068
3998
  }
4069
3999
 
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);
4000
+ // ../ingest/dist/workflows.js
4001
+ var newWorkflowStats = newSidecarStats;
4002
+ function toIso(x) {
4003
+ if (typeof x === "number" && Number.isFinite(x))
4004
+ return new Date(x).toISOString();
4005
+ if (typeof x === "string" && x) {
4006
+ const t = Date.parse(x);
4007
+ return Number.isNaN(t) ? null : new Date(t).toISOString();
4089
4008
  }
4009
+ return null;
4090
4010
  }
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;
4011
+ var jsonOrNull = (v) => v == null ? null : JSON.stringify(v);
4012
+ function cleanStr(v) {
4013
+ const s = strOrNull(v);
4014
+ if (!s)
4015
+ return null;
4016
+ return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\[[0-9;]+m\]?/g, "").trim() || null;
4101
4017
  }
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)
4018
+ function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
4019
+ const upsert = db.prepare(`INSERT INTO workflow_results
4020
+ (run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,
4021
+ result_json, phases_json, logs_json, progress_json,
4022
+ agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)
4115
4023
  VALUES
4116
- (@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)
4024
+ (@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,
4025
+ @result_json, @phases_json, @logs_json, @progress_json,
4026
+ @agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)
4027
+ ON CONFLICT(run_id) DO UPDATE SET
4028
+ source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,
4029
+ workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,
4030
+ default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,
4031
+ logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,
4032
+ total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
4033
+ duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
4034
+ ingested_at=excluded.ingested_at`);
4035
+ ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, {
4036
+ subdir: "workflows",
4037
+ // Only the run's own result file (wf_<id>.json); agent-*.meta.json / scripts are handled elsewhere.
4038
+ matchName: (name) => /^wf_.+\.json$/.test(name),
4039
+ handle: (path3, buf) => {
4040
+ let d;
4041
+ try {
4042
+ const parsed = JSON.parse(buf.toString("utf8"));
4043
+ if (!parsed || typeof parsed !== "object")
4044
+ return false;
4045
+ d = parsed;
4046
+ } catch {
4047
+ return false;
4048
+ }
4049
+ const runId = strOrNull(d.runId) ?? basename3(path3, ".json");
4050
+ const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
4051
+ const durationMs2 = intOrNull(d.durationMs);
4052
+ const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
4053
+ upsert.run({
4054
+ run_id: runId,
4055
+ source_id: sourceId,
4056
+ session_id: sessionBefore(path3, "workflows"),
4057
+ task_id: strOrNull(d.taskId),
4058
+ workflow_name: strOrNull(d.workflowName),
4059
+ status: strOrNull(d.status),
4060
+ summary: strOrNull(d.summary),
4061
+ default_model: cleanStr(d.defaultModel),
4062
+ result_json: jsonOrNull(d.result),
4063
+ phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,
4064
+ logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,
4065
+ progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,
4066
+ agent_count: intOrNull(d.agentCount),
4067
+ total_tokens: intOrNull(d.totalTokens),
4068
+ total_tool_calls: intOrNull(d.totalToolCalls),
4069
+ duration_ms: durationMs2,
4070
+ started_at: startedAt,
4071
+ ended_at: endedAt,
4072
+ ingested_at: now2
4073
+ });
4074
+ return true;
4075
+ }
4076
+ });
4077
+ }
4078
+
4079
+ // ../ingest/dist/meta.js
4080
+ import { basename as basename4 } from "path";
4081
+ var newMetaStats = newSidecarStats;
4082
+ function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now2, stats, full) {
4083
+ const upsert = db.prepare(`INSERT INTO session_meta
4084
+ (session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)
4085
+ VALUES
4086
+ (@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)
4117
4087
  ON CONFLICT(session_id) DO UPDATE SET
4118
4088
  source_id=excluded.source_id, agent_type=excluded.agent_type,
4119
4089
  agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
4120
4090
  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;
4149
- }
4150
- if (!d || typeof d !== "object") {
4151
- stats.malformed++;
4152
- continue;
4091
+ ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, {
4092
+ subdir: "subagents",
4093
+ matchName: (name) => /^agent-.+\.meta\.json$/.test(name),
4094
+ handle: (path3, buf) => {
4095
+ let d;
4096
+ try {
4097
+ const parsed = JSON.parse(buf.toString("utf8"));
4098
+ if (!parsed || typeof parsed !== "object")
4099
+ return false;
4100
+ d = parsed;
4101
+ } catch {
4102
+ return false;
4103
+ }
4104
+ upsert.run({
4105
+ session_id: basename4(path3, ".meta.json"),
4106
+ source_id: sourceId,
4107
+ agent_type: strOrNull(d.agentType),
4108
+ agent_description: strOrNull(d.description),
4109
+ spawn_depth: intOrNull(d.spawnDepth),
4110
+ tool_use_id: strOrNull(d.toolUseId),
4111
+ ingested_at: now2
4112
+ });
4113
+ return true;
4153
4114
  }
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
- }
4115
+ });
4167
4116
  }
4168
4117
 
4169
4118
  // ../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
- }
4119
+ import { basename as basename5 } from "path";
4120
+ var newToolResultStats = newSidecarStats;
4206
4121
  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
4122
  const upsert = db.prepare(`INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)
4216
4123
  VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
4217
4124
  ON CONFLICT(session_id, name) DO UPDATE SET
4218
4125
  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;
4126
+ ingestSidecars(db, sourceArchiveDir, excludedDirs, now2, stats, full, {
4127
+ subdir: "tool-results",
4128
+ matchName: (name) => name.endsWith(".txt"),
4129
+ // Skip a file outside the expected …/<sessionId>/tool-results/ layout (no files++, matching the
4130
+ // original early-continue before statting).
4131
+ include: (path3) => sessionBefore(path3, "tool-results") !== null,
4132
+ handle: (path3, buf, size) => {
4133
+ const sessionId = sessionBefore(path3, "tool-results");
4134
+ upsert.run({
4135
+ session_id: sessionId,
4136
+ name: basename5(path3, ".txt"),
4137
+ path: path3,
4138
+ bytes: size,
4139
+ text: buf.toString("utf8"),
4140
+ ingested_at: now2
4141
+ });
4142
+ return true;
4243
4143
  }
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
- }
4144
+ });
4255
4145
  }
4256
4146
 
4257
4147
  // ../ingest/dist/run.js
@@ -4267,6 +4157,45 @@ function parseIngestArgs(argv) {
4267
4157
  }
4268
4158
  return a;
4269
4159
  }
4160
+ function shouldReingest(path3, size, mtimeMs, prev) {
4161
+ if (prev && prev.size === size && prev.mtime_ms === mtimeMs)
4162
+ return { action: "skip-stat" };
4163
+ const small = size <= STREAM_THRESHOLD;
4164
+ const buf = small ? readFileSync5(path3) : null;
4165
+ const hash2 = small ? sha256(buf) : sha256File(path3);
4166
+ if (prev && prev.sha256 === hash2)
4167
+ return { action: "skip-hash", hash: hash2 };
4168
+ return { action: "ingest", hash: hash2, buf };
4169
+ }
4170
+ function printIngestReport(db, dbPath, r) {
4171
+ const count = (sql) => db.prepare(sql).get().n;
4172
+ const sessions = count("SELECT COUNT(*) n FROM sessions");
4173
+ const turns = count("SELECT COUNT(*) n FROM turns");
4174
+ const events = count("SELECT COUNT(*) n FROM events");
4175
+ const tools = count("SELECT COUNT(*) n FROM tool_calls");
4176
+ const usageRows = db.prepare("SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage").all();
4177
+ let cost = 0;
4178
+ let totalTokens = 0;
4179
+ for (const u of usageRows) {
4180
+ cost += costForUsage(u.model, u);
4181
+ totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
4182
+ }
4183
+ const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
4184
+ const metaRows = count("SELECT COUNT(*) n FROM session_meta");
4185
+ const trRows = count("SELECT COUNT(*) n FROM tool_results");
4186
+ const sevCounts = db.prepare("SELECT severity, COUNT(*) n FROM findings GROUP BY severity").all();
4187
+ const sevMap = Object.fromEntries(sevCounts.map((s) => [s.severity, s.n]));
4188
+ const sevLine = ["critical", "high", "medium", "low", "info"].map((s) => `${s}=${sevMap[s] ?? 0}`).join(" ");
4189
+ db.close();
4190
+ 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}` : ""}
4191
+ sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${r.classified.count}
4192
+ workflow_results=${wfRuns} (sidecar upserted=${r.wfStats.upserted} skipped=${r.wfStats.skipped} malformed=${r.wfStats.malformed})
4193
+ session_meta=${metaRows} (upserted=${r.metaStats.upserted} skipped=${r.metaStats.skipped} malformed=${r.metaStats.malformed})
4194
+ tool_results=${trRows} (upserted=${r.trStats.upserted} skipped=${r.trStats.skipped} malformed=${r.trStats.malformed})
4195
+ findings=${r.detected.count} (${sevLine})
4196
+ file_changes=${r.fileChanges.count}
4197
+ tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
4198
+ }
4270
4199
  function runIngest(argv = process.argv.slice(2)) {
4271
4200
  const args = parseIngestArgs(argv);
4272
4201
  const archiveRoot = args.archive || resolveArchiveDir();
@@ -4308,41 +4237,39 @@ function runIngest(argv = process.argv.slice(2)) {
4308
4237
  }
4309
4238
  stmts.insAgent.run(adapter.agentId, adapter.agentName);
4310
4239
  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);
4240
+ let files = adapter.discover(join8(archiveRoot, source.label), source.label);
4312
4241
  if (excludedDirs.length)
4313
4242
  files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
4314
4243
  files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
4315
4244
  for (const file of files) {
4316
4245
  stats.files++;
4317
- const st = statSync5(file.path);
4246
+ const st = statSync3(file.path);
4318
4247
  const mtimeMs = Math.trunc(st.mtimeMs);
4319
4248
  const prev = args.full ? void 0 : stmts.getState.get(file.path);
4320
- if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
4249
+ const decision = shouldReingest(file.path, st.size, mtimeMs, prev);
4250
+ if (decision.action === "skip-stat") {
4321
4251
  stats.skipped++;
4322
4252
  continue;
4323
4253
  }
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) {
4254
+ if (decision.action === "skip-hash") {
4328
4255
  stmts.setState.run({
4329
4256
  file_path: file.path,
4330
4257
  size: st.size,
4331
4258
  mtime_ms: mtimeMs,
4332
- sha256: hash2,
4259
+ sha256: decision.hash,
4333
4260
  events_ingested: prev.events_ingested,
4334
4261
  ingested_at: now2
4335
4262
  });
4336
4263
  stats.skipped++;
4337
4264
  continue;
4338
4265
  }
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);
4266
+ const lines = decision.buf ? decision.buf.toString("utf8").split("\n") : streamLines(file.path);
4267
+ ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: decision.hash }, now2, stats);
4341
4268
  dirty.add(file.sessionId);
4342
4269
  }
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);
4270
+ ingestWorkflowResults(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
4271
+ ingestSubagentMeta(db, join8(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
4272
+ ingestToolResults(db, join8(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
4346
4273
  }
4347
4274
  const expanded = rebuildDerived(db, args.full ? null : dirty);
4348
4275
  canonicalizeProjects(db);
@@ -4350,33 +4277,7 @@ function runIngest(argv = process.argv.slice(2)) {
4350
4277
  const detected = detect(db, args.full ? null : expanded);
4351
4278
  classifyErrors(db, args.full ? null : expanded);
4352
4279
  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}`);
4280
+ printIngestReport(db, dbPath, { stats, pruned, classified, detected, fileChanges, wfStats, metaStats, trStats });
4380
4281
  }
4381
4282
  function runMetrics(argv = process.argv.slice(2)) {
4382
4283
  let dbArg = "";
@@ -4405,6 +4306,84 @@ import { fileURLToPath as fileURLToPath2 } from "url";
4405
4306
 
4406
4307
  // ../server/dist/db.js
4407
4308
  import Database2 from "better-sqlite3";
4309
+
4310
+ // ../server/dist/sql-util.js
4311
+ function tableExists(db, name) {
4312
+ return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
4313
+ }
4314
+ function queryAll(db, sql, ...params) {
4315
+ return db.prepare(sql).all(...params);
4316
+ }
4317
+ function pushDateRange(clauses, params, col, from, to) {
4318
+ if (from) {
4319
+ clauses.push(`date(${col}) >= date(?)`);
4320
+ params.push(from);
4321
+ }
4322
+ if (to) {
4323
+ clauses.push(`date(${col}) <= date(?)`);
4324
+ params.push(to);
4325
+ }
4326
+ }
4327
+ function metaJoin(hasMeta) {
4328
+ return hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : "";
4329
+ }
4330
+ function metaProjection(hasMeta) {
4331
+ return hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth";
4332
+ }
4333
+ function orderBy(columns, sort, fallback, dir) {
4334
+ const col = sort !== void 0 && Object.hasOwn(columns, sort) ? columns[sort] : columns[fallback];
4335
+ return `${col} ${dir === "asc" ? "ASC" : "DESC"}`;
4336
+ }
4337
+ function pageLimit(raw, fallback, max) {
4338
+ const n = Math.floor(Number(raw));
4339
+ if (!Number.isFinite(n) || n < 1)
4340
+ return Math.min(fallback, max);
4341
+ return Math.min(n, max);
4342
+ }
4343
+ function pageOffset(raw) {
4344
+ const n = Math.floor(Number(raw));
4345
+ return Number.isFinite(n) && n > 0 ? n : 0;
4346
+ }
4347
+ function pushGrouped(map, key, value) {
4348
+ const list = map.get(key);
4349
+ if (list)
4350
+ list.push(value);
4351
+ else
4352
+ map.set(key, [value]);
4353
+ }
4354
+
4355
+ // ../server/dist/usage.js
4356
+ var USAGE_SUMS = "SUM(input_tokens) i, SUM(output_tokens) o, SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr";
4357
+ function sessionUsage(db, sessionId) {
4358
+ return queryAll(db, `SELECT model, ${USAGE_SUMS} FROM token_usage WHERE session_id = ? GROUP BY model`, sessionId);
4359
+ }
4360
+ function splitOf(rows) {
4361
+ const split = { input: 0, output: 0, cache_creation: 0, cache_read: 0 };
4362
+ for (const u of rows) {
4363
+ split.input += u.i;
4364
+ split.output += u.o;
4365
+ split.cache_creation += u.cw;
4366
+ split.cache_read += u.cr;
4367
+ }
4368
+ return split;
4369
+ }
4370
+ function tokensOf(split) {
4371
+ return split.input + split.output + split.cache_creation + split.cache_read;
4372
+ }
4373
+ function costOf(rows) {
4374
+ let cost = 0;
4375
+ for (const u of rows) {
4376
+ cost += costForUsage(u.model, {
4377
+ input_tokens: u.i,
4378
+ output_tokens: u.o,
4379
+ cache_creation_input_tokens: u.cw,
4380
+ cache_read_input_tokens: u.cr
4381
+ });
4382
+ }
4383
+ return Number(cost.toFixed(4));
4384
+ }
4385
+
4386
+ // ../server/dist/db.js
4408
4387
  function openReadonly(file) {
4409
4388
  const db = new Database2(file, { readonly: true, fileMustExist: true });
4410
4389
  db.pragma("query_only = ON");
@@ -4467,10 +4446,7 @@ function listSessions(db, f) {
4467
4446
  where.push("s.source_id = ?"), params.push(f.source);
4468
4447
  if (f.project)
4469
4448
  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);
4449
+ pushDateRange(where, params, "s.started_at", f.from, f.to);
4474
4450
  if (f.kind === "main")
4475
4451
  where.push("s.is_sidechain = 0");
4476
4452
  if (f.kind === "subagent")
@@ -4514,7 +4490,6 @@ function listSessions(db, f) {
4514
4490
  duration: "s.duration_ms"
4515
4491
  };
4516
4492
  const sortKey = f.sort ?? "started";
4517
- const sqlDir = f.dir === "asc" ? "ASC" : "DESC";
4518
4493
  const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
4519
4494
  let rows;
4520
4495
  if (JS_SORT.has(sortKey)) {
@@ -4542,55 +4517,35 @@ function listSessions(db, f) {
4542
4517
  });
4543
4518
  rows = all.slice(f.offset, f.offset + f.limit);
4544
4519
  } 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);
4520
+ rows = db.prepare(`${baseSelect} ORDER BY ${orderBy(NATIVE_ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`).all(...params, f.limit, f.offset);
4547
4521
  attachSessionCost(db, rows);
4548
4522
  }
4549
4523
  return { total, sessions: rows };
4550
4524
  }
4551
4525
  function attachSessionCost(db, rows) {
4552
- const costBySession = /* @__PURE__ */ new Map();
4526
+ const usageBySession = /* @__PURE__ */ new Map();
4553
4527
  const ids = rows.map((r) => r.id);
4554
4528
  const CHUNK3 = 800;
4555
4529
  for (let i = 0; i < ids.length; i += CHUNK3) {
4556
4530
  const batch = ids.slice(i, i + CHUNK3);
4557
4531
  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);
4532
+ 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
4533
  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);
4534
+ const list = usageBySession.get(u.session_id) ?? [];
4535
+ list.push(u);
4536
+ usageBySession.set(u.session_id, list);
4576
4537
  }
4577
4538
  }
4578
4539
  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;
4540
+ const u = usageBySession.get(r.id) ?? [];
4541
+ const split = splitOf(u);
4542
+ r.tokens = tokensOf(split);
4543
+ r.token_split = split;
4544
+ r.cost = costOf(u);
4583
4545
  r.title = r.ai_title || r.slug || null;
4584
4546
  }
4585
4547
  }
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);
4548
+ function loadToolCalls(db, id) {
4594
4549
  const toolRows = db.prepare(`SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
4595
4550
  workflow_run_id, workflow_name, status, error_type,
4596
4551
  total_duration_ms, total_tokens, input_json, result_summary,
@@ -4607,40 +4562,48 @@ function getSession(db, id) {
4607
4562
  t.full_result = { text: full.text, bytes: full.bytes };
4608
4563
  }
4609
4564
  }
4565
+ return toolRows;
4566
+ }
4567
+ function loadFindings(db, id, toolRows) {
4568
+ if (!tableExists(db, "findings"))
4569
+ return [];
4610
4570
  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));
4571
+ const findingRows = db.prepare(`SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json
4572
+ FROM findings WHERE session_id = ?`).all(id);
4573
+ const byToolCall = /* @__PURE__ */ new Map();
4574
+ for (const fr of findingRows) {
4575
+ const f = {
4576
+ id: fr.id,
4577
+ tool_call_id: fr.tool_call_id,
4578
+ event_uuid: fr.event_uuid,
4579
+ turn_id: fr.turn_id,
4580
+ rule_id: fr.rule_id,
4581
+ category: fr.category,
4582
+ framework_ref: fr.framework_ref,
4583
+ severity: fr.severity,
4584
+ title: fr.title,
4585
+ evidence: fr.evidence,
4586
+ signals: safeJson(fr.signals_json)
4587
+ };
4588
+ sessionFindings.push(f);
4589
+ if (fr.tool_call_id)
4590
+ pushGrouped(byToolCall, fr.tool_call_id, f);
4636
4591
  }
4592
+ for (const t of toolRows)
4593
+ t.findings = byToolCall.get(t.id) ?? [];
4594
+ sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
4595
+ return sessionFindings;
4596
+ }
4597
+ function loadEvents(db, id, toolRows) {
4598
+ const eventRows = db.prepare(`SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
4599
+ FROM events WHERE session_id = ? ORDER BY timestamp, seq`).all(id);
4637
4600
  const toolsByEvent = /* @__PURE__ */ new Map();
4638
4601
  for (const t of toolRows) {
4639
4602
  if (!t.event_uuid)
4640
4603
  continue;
4641
- (toolsByEvent.get(t.event_uuid) ?? toolsByEvent.set(t.event_uuid, []).get(t.event_uuid)).push(t);
4604
+ pushGrouped(toolsByEvent, t.event_uuid, t);
4642
4605
  }
4643
- const events = eventRows.map((e) => {
4606
+ return eventRows.map((e) => {
4644
4607
  const { text, thinking } = extractParts(e.raw_json);
4645
4608
  return {
4646
4609
  uuid: e.uuid,
@@ -4655,91 +4618,101 @@ function getSession(db, id) {
4655
4618
  toolCalls: toolsByEvent.get(e.uuid) ?? []
4656
4619
  };
4657
4620
  });
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;
4621
+ }
4622
+ function loadClassification(db, id) {
4679
4623
  const cls = db.prepare(`SELECT category, complexity_score, complexity_band, signals_json, classifier_version
4680
4624
  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 };
4625
+ if (!cls)
4626
+ return null;
4627
+ let signals = null;
4628
+ try {
4629
+ signals = cls.signals_json ? JSON.parse(cls.signals_json) : null;
4630
+ } catch {
4703
4631
  }
4632
+ return {
4633
+ category: cls.category,
4634
+ complexity_score: cls.complexity_score,
4635
+ complexity_band: cls.complexity_band,
4636
+ classifier_version: cls.classifier_version,
4637
+ signals
4638
+ };
4639
+ }
4640
+ function loadParent(db, session) {
4641
+ if (!session.parent_session_id)
4642
+ return null;
4643
+ const p = db.prepare(`SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
4644
+ FROM sessions ps LEFT JOIN turns t ON t.id = ?
4645
+ WHERE ps.id = ?`).get(session.parent_turn_id, session.parent_session_id);
4646
+ return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;
4647
+ }
4648
+ function loadChildren(db, id) {
4704
4649
  const hasMeta = tableExists(db, "session_meta");
4705
4650
  const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
4706
4651
  (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" : ""}
4652
+ ${metaProjection(hasMeta)}
4653
+ FROM sessions s ${metaJoin(hasMeta)}
4709
4654
  WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
4710
4655
  for (const ch of children) {
4711
4656
  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));
4657
+ const u = sessionUsage(db, ch.id);
4658
+ ch.tokens = tokensOf(splitOf(u));
4659
+ ch.cost = costOf(u);
4717
4660
  }
4661
+ return children;
4662
+ }
4663
+ function loadWorkflowRuns(db, id) {
4718
4664
  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,
4665
+ return db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
4720
4666
  (SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
4721
4667
  ${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
4722
4668
  FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
4723
4669
  WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
4724
4670
  ORDER BY t.seq`).all(id);
4671
+ }
4672
+ function loadFileChanges(db, id) {
4673
+ if (!tableExists(db, "file_changes"))
4674
+ return [];
4675
+ return db.prepare(`SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
4676
+ fc.lines_added, fc.lines_removed, fc.timestamp
4677
+ FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`).all(id);
4678
+ }
4679
+ function attachSessionTotals(db, session, id, toolRows) {
4680
+ const usage = sessionUsage(db, id);
4681
+ const split = splitOf(usage);
4682
+ session.tokens = tokensOf(split);
4683
+ session.token_split = split;
4684
+ session.cost = costOf(usage);
4685
+ session.title = session.ai_title || session.slug || null;
4725
4686
  session.tool_call_count = toolRows.length;
4726
4687
  session.tool_error_count = toolRows.filter((t) => t.status === "error").length;
4727
4688
  session.tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
4728
4689
  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 };
4690
+ }
4691
+ function getSession(db, id) {
4692
+ const session = db.prepare(`SELECT s.*, p.path AS project_path FROM sessions s
4693
+ LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`).get(id);
4694
+ if (!session)
4695
+ return null;
4696
+ const turns = db.prepare("SELECT * FROM turns WHERE session_id = ? ORDER BY seq").all(id);
4697
+ const toolRows = loadToolCalls(db, id);
4698
+ const findings = loadFindings(db, id, toolRows);
4699
+ const events = loadEvents(db, id, toolRows);
4700
+ attachSessionTotals(db, session, id, toolRows);
4701
+ return {
4702
+ session,
4703
+ turns,
4704
+ events,
4705
+ classification: loadClassification(db, id),
4706
+ parent: loadParent(db, session),
4707
+ children: loadChildren(db, id),
4708
+ workflow_runs: loadWorkflowRuns(db, id),
4709
+ findings,
4710
+ file_changes: loadFileChanges(db, id)
4711
+ };
4736
4712
  }
4737
4713
  function xmlTag(text, tag) {
4738
4714
  return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
4739
4715
  }
4740
- function tableExists(db, name) {
4741
- return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
4742
- }
4743
4716
  function safeJson(s) {
4744
4717
  if (!s)
4745
4718
  return null;
@@ -4749,24 +4722,12 @@ function safeJson(s) {
4749
4722
  return null;
4750
4723
  }
4751
4724
  }
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;
4725
+ function loadAgents(db, runId) {
4765
4726
  const hasMeta = tableExists(db, "session_meta");
4766
4727
  const agents = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
4767
4728
  (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" : ""}
4729
+ ${metaProjection(hasMeta)}
4730
+ FROM sessions s ${metaJoin(hasMeta)}
4770
4731
  WHERE s.workflow_run_id = ? ORDER BY s.started_at`).all(runId);
4771
4732
  let total_tokens = 0;
4772
4733
  let total_cost = 0;
@@ -4774,11 +4735,9 @@ function getWorkflow(db, runId) {
4774
4735
  let ended_at = null;
4775
4736
  for (const a of agents) {
4776
4737
  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));
4738
+ const u = sessionUsage(db, a.id);
4739
+ a.tokens = tokensOf(splitOf(u));
4740
+ a.cost = costOf(u);
4782
4741
  total_tokens += a.tokens;
4783
4742
  total_cost += a.cost;
4784
4743
  if (a.started_at && (!started_at || a.started_at < started_at))
@@ -4787,47 +4746,70 @@ function getWorkflow(db, runId) {
4787
4746
  ended_at = a.ended_at;
4788
4747
  }
4789
4748
  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
- }
4749
+ return {
4750
+ agents,
4751
+ stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
4752
+ };
4753
+ }
4754
+ function parseCompletion(db, wf) {
4755
+ if (!wf.tool_use_id || !wf.parent_session_id)
4756
+ return null;
4757
+ const ev = db.prepare(`SELECT raw_json FROM events
4758
+ WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
4759
+ ORDER BY timestamp DESC LIMIT 1`).get(wf.parent_session_id, `%${wf.tool_use_id}%`);
4760
+ if (!ev)
4761
+ return null;
4762
+ const { text } = extractParts(ev.raw_json);
4763
+ const t = text ?? "";
4764
+ return { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
4765
+ }
4766
+ function loadRunSidecar(db, runId, completion) {
4801
4767
  const wr = tableExists(db, "workflow_results") ? db.prepare(`SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
4802
4768
  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)
4769
+ FROM workflow_results WHERE run_id = ?`).get(runId) : null;
4770
+ if (!wr)
4771
+ return { wr: null, run: null, completion };
4772
+ const run = {
4773
+ status: wr.status ?? null,
4774
+ summary: wr.summary ?? null,
4775
+ default_model: wr.default_model ?? null,
4776
+ agent_count: wr.agent_count ?? null,
4777
+ total_tokens: wr.total_tokens ?? null,
4778
+ total_tool_calls: wr.total_tool_calls ?? null,
4779
+ duration_ms: wr.duration_ms ?? null,
4780
+ started_at: wr.started_at ?? null,
4781
+ ended_at: wr.ended_at ?? null,
4782
+ phases: safeJson(wr.phases_json),
4783
+ logs: safeJson(wr.logs_json),
4784
+ // The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
4785
+ // with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.
4786
+ progress: safeJson(wr.progress_json)
4787
+ };
4788
+ if (wr.result_json != null || wr.status) {
4789
+ completion = {
4790
+ status: wr.status ?? completion?.status ?? null,
4791
+ summary: wr.summary ?? completion?.summary ?? null,
4792
+ result: wr.result_json ?? completion?.result ?? null,
4793
+ failures: completion?.failures ?? null
4821
4794
  };
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
4795
  }
4796
+ return { wr, run, completion };
4797
+ }
4798
+ function getWorkflow(db, runId) {
4799
+ const wf = db.prepare(`SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,
4800
+ tc.input_json AS input_json,
4801
+ tc.session_id AS parent_session_id, t.seq AS turn_seq,
4802
+ ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
4803
+ FROM tool_calls tc
4804
+ LEFT JOIN turns t ON t.id = tc.turn_id
4805
+ LEFT JOIN sessions ps ON ps.id = tc.session_id
4806
+ WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
4807
+ ORDER BY t.seq
4808
+ LIMIT 1`).get(runId);
4809
+ if (!wf)
4810
+ return null;
4811
+ const { agents, stats } = loadAgents(db, runId);
4812
+ const { wr, run, completion } = loadRunSidecar(db, wf.run_id, parseCompletion(db, wf));
4831
4813
  return {
4832
4814
  run_id: wf.run_id,
4833
4815
  name: wf.name ?? null,
@@ -4838,7 +4820,7 @@ function getWorkflow(db, runId) {
4838
4820
  run,
4839
4821
  parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },
4840
4822
  agents,
4841
- stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
4823
+ stats
4842
4824
  };
4843
4825
  }
4844
4826
  function listSkills(db, f = {}) {
@@ -4897,13 +4879,11 @@ function listFiles(db, f = {}) {
4897
4879
  sessions: "sessions",
4898
4880
  path: "file_path"
4899
4881
  };
4900
- const col = ORDER[f.sort ?? "last_ts"] ?? "last_ts";
4901
- const dir = f.dir === "asc" ? "ASC" : "DESC";
4902
4882
  const limit = Math.min(f.limit ?? 50, 200);
4903
4883
  const offset = f.offset ?? 0;
4904
4884
  const files = db.prepare(`SELECT g.*, p.path AS project_path FROM (${grouped}) g
4905
4885
  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);
4886
+ ORDER BY ${orderBy(ORDER, f.sort, "last_ts", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`).all(...params, limit, offset);
4907
4887
  return { total, files };
4908
4888
  }
4909
4889
  function getFileTimeline(db, filePath, projectId2) {
@@ -5010,10 +4990,7 @@ function findingWhere(f) {
5010
4990
  sql.push("s.source_id = ?"), params.push(f.source);
5011
4991
  if (f.project)
5012
4992
  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);
4993
+ pushDateRange(sql, params, "s.started_at", f.from, f.to);
5017
4994
  return { sql, params };
5018
4995
  }
5019
4996
  function statusWhere(status, attached) {
@@ -5048,8 +5025,6 @@ function listFindings(db, f) {
5048
5025
  category: "f.category",
5049
5026
  time: "s.started_at"
5050
5027
  };
5051
- const orderCol = ORDER[f.sort ?? "severity"] ?? SEVERITY_RANK_SQL;
5052
- const dir = f.dir === "asc" ? "ASC" : "DESC";
5053
5028
  const findings = db.prepare(`SELECT f.id, f.session_id, f.tool_call_id, f.event_uuid, f.turn_id, f.rule_id, f.category,
5054
5029
  f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,
5055
5030
  COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,
@@ -5060,7 +5035,7 @@ function listFindings(db, f) {
5060
5035
  LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id
5061
5036
  ${dismissJoin}
5062
5037
  ${whereSql}
5063
- ORDER BY ${orderCol} ${dir}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5038
+ ORDER BY ${orderBy(ORDER, f.sort, "severity", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
5064
5039
  LIMIT ? OFFSET ?`).all(...w.params, f.limit, f.offset);
5065
5040
  return { total, findings };
5066
5041
  }
@@ -5115,7 +5090,11 @@ function renderSessionExport(db, id, level) {
5115
5090
  }));
5116
5091
  const { markdown } = exportMarkdown({ id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at }, events, { level });
5117
5092
  const suffix = level === "off" ? "" : ".redacted";
5118
- return { markdown, filename: `session-${id.slice(0, 8)}${suffix}.md` };
5093
+ return { markdown, filename: `session-${filenameSafe(id.slice(0, 8))}${suffix}.md` };
5094
+ }
5095
+ function filenameSafe(s) {
5096
+ const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
5097
+ return cleaned === "" ? "session" : cleaned;
5119
5098
  }
5120
5099
 
5121
5100
  // ../server/dist/dashboard.js
@@ -5124,10 +5103,7 @@ function sessionWhere(f) {
5124
5103
  const params = [];
5125
5104
  if (f.source)
5126
5105
  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);
5106
+ pushDateRange(where, params, "s.started_at", f.from, f.to);
5131
5107
  return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
5132
5108
  }
5133
5109
  var zeroSplit = () => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });
@@ -5143,23 +5119,17 @@ function pct(sorted, p) {
5143
5119
  const rank = Math.ceil(p / 100 * sorted.length) - 1;
5144
5120
  return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)];
5145
5121
  }
5146
- function tableExists2(db, name) {
5147
- return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
5148
- }
5149
5122
  function workflowWhere(f) {
5150
5123
  const where = [];
5151
5124
  const params = [];
5152
5125
  if (f.source)
5153
5126
  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);
5127
+ pushDateRange(where, params, "started_at", f.from, f.to);
5158
5128
  return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params };
5159
5129
  }
5160
5130
  function workflowAgg(db, f) {
5161
5131
  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"))
5132
+ if (!tableExists(db, "workflow_results"))
5163
5133
  return empty;
5164
5134
  const ww = workflowWhere(f);
5165
5135
  const rows = db.prepare(`SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,
@@ -5300,14 +5270,14 @@ function dashboardTimeseries(db, f, bucketParam) {
5300
5270
  r.cost = Number(r.cost.toFixed(4));
5301
5271
  return { bucket, series };
5302
5272
  }
5303
- function dashboardBreakdowns(db, f) {
5304
- const w = sessionWhere(f);
5273
+ var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
5274
+ function modelBreakdown(db, w) {
5305
5275
  const modelRows = db.prepare(`SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
5306
5276
  SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
5307
5277
  COUNT(DISTINCT t.session_id) sessions
5308
5278
  FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
5309
5279
  GROUP BY t.model ORDER BY (SUM(t.input_tokens)+SUM(t.output_tokens)+SUM(t.cache_creation_input_tokens)+SUM(t.cache_read_input_tokens)) DESC`).all(...w.params);
5310
- const byModel = modelRows.map((u) => ({
5280
+ return modelRows.map((u) => ({
5311
5281
  model: u.model,
5312
5282
  tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
5313
5283
  total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
@@ -5315,38 +5285,52 @@ function dashboardBreakdowns(db, f) {
5315
5285
  sessions: u.sessions ?? 0,
5316
5286
  priced: !!rateForModel(u.model)
5317
5287
  }));
5318
- const bySource = db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5288
+ }
5289
+ function sourceBreakdown(db, w) {
5290
+ return db.prepare(`SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
5319
5291
  FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`).all(...w.params);
5292
+ }
5293
+ function classificationBreakdowns(db, w) {
5320
5294
  const mainW = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
5321
5295
  const byCategory = db.prepare(`SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5322
5296
  ${mainW.sql} GROUP BY c.category ORDER BY n DESC`).all(...mainW.params);
5323
5297
  const byComplexity = db.prepare(`SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
5324
5298
  ${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
5299
+ return { byCategory, byComplexity };
5300
+ }
5301
+ function toolBreakdown(db, w) {
5302
+ return db.prepare(`SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5326
5303
  ${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5304
+ }
5305
+ function skillBreakdowns(db, w) {
5327
5306
  const skills = db.prepare(`SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
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);
5307
+ ${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`).all(...w.params);
5329
5308
  const skillVersions = db.prepare(`SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
5330
5309
  FROM tool_calls tc
5331
5310
  JOIN skills sk ON sk.id = tc.skill_id
5332
5311
  JOIN sessions s ON s.id = tc.session_id
5333
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.skill_id IS NOT NULL
5312
+ ${andWhere(w)} tc.skill_id IS NOT NULL
5334
5313
  GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`).all(...w.params);
5314
+ return { skills, skillVersions };
5315
+ }
5316
+ function subagentFanoutBreakdown(db, w) {
5335
5317
  const subagentByType = db.prepare(`SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
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);
5318
+ ${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`).all(...w.params);
5337
5319
  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')
5320
+ ${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
5339
5321
  GROUP BY tc.session_id`).all(...w.params);
5340
5322
  const counts = perSession.map((r) => r.n);
5341
- const subagentFanout = {
5323
+ return {
5342
5324
  by_type: subagentByType,
5343
5325
  sessions_with_subagents: counts.length,
5344
5326
  total_spawns: counts.reduce((a, b) => a + b, 0),
5345
5327
  max_per_session: counts.length ? Math.max(...counts) : 0,
5346
5328
  avg_per_session: counts.length ? Number((counts.reduce((a, b) => a + b, 0) / counts.length).toFixed(2)) : 0
5347
5329
  };
5330
+ }
5331
+ function errorTypeBreakdown(db, w) {
5348
5332
  const errorRows = db.prepare(`SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
5349
- ${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
5333
+ ${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`).all(...w.params);
5350
5334
  let failures = 0;
5351
5335
  let rejections = 0;
5352
5336
  const byType = errorRows.map((r) => {
@@ -5357,19 +5341,34 @@ function dashboardBreakdowns(db, f) {
5357
5341
  failures += r.n;
5358
5342
  return { type: r.type, kind, n: r.n };
5359
5343
  });
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 };
5344
+ return { by_type: byType, failures, rejections };
5345
+ }
5346
+ function dashboardBreakdowns(db, f) {
5347
+ const w = sessionWhere(f);
5348
+ const { byCategory, byComplexity } = classificationBreakdowns(db, w);
5349
+ const { skills, skillVersions } = skillBreakdowns(db, w);
5350
+ return {
5351
+ by_model: modelBreakdown(db, w),
5352
+ by_source: sourceBreakdown(db, w),
5353
+ by_category: byCategory,
5354
+ by_complexity: byComplexity,
5355
+ tools: toolBreakdown(db, w),
5356
+ skills,
5357
+ skill_versions: skillVersions,
5358
+ subagent_fanout: subagentFanoutBreakdown(db, w),
5359
+ error_types: errorTypeBreakdown(db, w)
5360
+ };
5362
5361
  }
5363
5362
 
5364
5363
  // ../server/dist/refresh.js
5365
5364
  import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
5366
- import { join as join11 } from "path";
5365
+ import { join as join9 } from "path";
5366
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5367
5367
  function originAllowed(origin) {
5368
5368
  if (!origin)
5369
5369
  return true;
5370
5370
  try {
5371
- const h = new URL(origin).hostname;
5372
- return h === "127.0.0.1" || h === "localhost" || h === "::1";
5371
+ return LOOPBACK_HOSTS.has(new URL(origin).hostname);
5373
5372
  } catch {
5374
5373
  return false;
5375
5374
  }
@@ -5384,7 +5383,7 @@ function writeBlocked(headers) {
5384
5383
  }
5385
5384
  function runRefresh() {
5386
5385
  const dataDir = resolveDataDir(findRepoRoot());
5387
- const lock = acquireLock(join11(dataDir, ".agent-lens.lock"));
5386
+ const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
5388
5387
  if (!lock)
5389
5388
  return null;
5390
5389
  try {
@@ -5487,12 +5486,14 @@ function setPref(db, key, value) {
5487
5486
  }
5488
5487
 
5489
5488
  // ../server/dist/app.js
5490
- var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
5489
+ function notFound(reply, message = "not found") {
5490
+ return reply.code(404).send({ error: { code: "NOT_FOUND", message } });
5491
+ }
5491
5492
  async function createApp(db, opts = {}) {
5492
5493
  const app = Fastify({ logger: false });
5493
5494
  if (opts.enforceLoopbackHost !== false) {
5494
5495
  app.addHook("onRequest", async (req, reply) => {
5495
- const host = (req.headers.host ?? "").replace(/:\d+$/, "");
5496
+ const host = (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
5496
5497
  if (!LOOPBACK_HOSTS.has(host)) {
5497
5498
  return reply.code(403).send({ error: { code: "FORBIDDEN_HOST", message: "non-loopback Host rejected" } });
5498
5499
  }
@@ -5564,7 +5565,7 @@ async function createApp(db, opts = {}) {
5564
5565
  if (!triageDb)
5565
5566
  return { value: null };
5566
5567
  const raw = getPref(triageDb, key);
5567
- return { value: raw == null ? null : JSON.parse(raw) };
5568
+ return { value: safeJson(raw) };
5568
5569
  });
5569
5570
  app.put("/api/prefs/:key", async (req, reply) => {
5570
5571
  const { key } = req.params;
@@ -5590,22 +5591,22 @@ async function createApp(db, opts = {}) {
5590
5591
  errorType: q.error_type ? q.error_type.split(",").filter(Boolean) : void 0,
5591
5592
  sort: ["started", "title", "turns", "tokens", "cost", "duration", "errors", "security"].find((s) => s === q.sort),
5592
5593
  dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
5593
- limit: Math.min(Number(q.limit) || 50, 200),
5594
- offset: Number(q.offset) || 0
5594
+ limit: pageLimit(q.limit, 50, 200),
5595
+ offset: pageOffset(q.offset)
5595
5596
  });
5596
5597
  });
5597
5598
  app.get("/api/sessions/:id", async (req, reply) => {
5598
5599
  const { id } = req.params;
5599
5600
  const result = getSession(db, id);
5600
5601
  if (!result)
5601
- return reply.code(404).send({ error: "not found" });
5602
+ return notFound(reply);
5602
5603
  return result;
5603
5604
  });
5604
5605
  app.get("/api/workflows/:run_id", async (req, reply) => {
5605
5606
  const { run_id } = req.params;
5606
5607
  const result = getWorkflow(db, run_id);
5607
5608
  if (!result)
5608
- return reply.code(404).send({ error: "not found" });
5609
+ return notFound(reply);
5609
5610
  return result;
5610
5611
  });
5611
5612
  app.get("/api/security/summary", async () => securitySummary(db));
@@ -5626,8 +5627,8 @@ async function createApp(db, opts = {}) {
5626
5627
  ...findingFilters(q),
5627
5628
  sort: ["severity", "session", "rule", "category", "time"].find((s) => s === q.sort),
5628
5629
  dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
5629
- limit: Math.min(Number(q.limit) || 50, 5e3),
5630
- offset: Number(q.offset) || 0
5630
+ limit: pageLimit(q.limit, 50, 5e3),
5631
+ offset: pageOffset(q.offset)
5631
5632
  });
5632
5633
  });
5633
5634
  app.get("/api/security/mutes", async () => triageDb ? listMutes(triageDb) : []);
@@ -5680,9 +5681,15 @@ async function createApp(db, opts = {}) {
5680
5681
  });
5681
5682
  app.get("/api/skills/:name", async (req, reply) => {
5682
5683
  const { name } = req.params;
5683
- const result = getSkill(db, decodeURIComponent(name));
5684
+ let decoded;
5685
+ try {
5686
+ decoded = decodeURIComponent(name);
5687
+ } catch {
5688
+ return reply.code(400).send({ error: { code: "BAD_NAME", message: "invalid skill name encoding" } });
5689
+ }
5690
+ const result = getSkill(db, decoded);
5684
5691
  if (!result)
5685
- return reply.code(404).send({ error: "not found" });
5692
+ return notFound(reply);
5686
5693
  return result;
5687
5694
  });
5688
5695
  app.get("/api/files", async (req) => {
@@ -5694,17 +5701,17 @@ async function createApp(db, opts = {}) {
5694
5701
  project: q.project,
5695
5702
  sort,
5696
5703
  dir: q.dir === "asc" ? "asc" : "desc",
5697
- limit: Math.min(Number(q.limit) || 50, 200),
5698
- offset: Number(q.offset) || 0
5704
+ limit: pageLimit(q.limit, 50, 200),
5705
+ offset: pageOffset(q.offset)
5699
5706
  });
5700
5707
  });
5701
5708
  app.get("/api/file", async (req, reply) => {
5702
5709
  const q = req.query;
5703
5710
  if (!q.path)
5704
- return reply.code(400).send({ error: "missing path" });
5711
+ return reply.code(400).send({ error: { code: "MISSING_PATH", message: "missing path" } });
5705
5712
  const result = getFileTimeline(db, q.path, q.project || void 0);
5706
5713
  if (!result)
5707
- return reply.code(404).send({ error: "not found" });
5714
+ return notFound(reply);
5708
5715
  return result;
5709
5716
  });
5710
5717
  app.get("/api/sessions/:id/export.md", async (req, reply) => {
@@ -5712,14 +5719,14 @@ async function createApp(db, opts = {}) {
5712
5719
  const level = parseRedactionLevel(req.query.redact);
5713
5720
  const out = renderSessionExport(db, id, level);
5714
5721
  if (!out)
5715
- return reply.code(404).send({ error: "not found" });
5722
+ return notFound(reply);
5716
5723
  reply.header("content-type", "text/markdown; charset=utf-8").header("content-disposition", `attachment; filename="${out.filename}"`).send(out.markdown);
5717
5724
  });
5718
5725
  if (opts.webDist && existsSync7(opts.webDist)) {
5719
5726
  await app.register(fastifyStatic, { root: opts.webDist });
5720
5727
  app.setNotFoundHandler((req, reply) => {
5721
5728
  if (req.url.startsWith("/api/"))
5722
- return reply.code(404).send({ error: "not found" });
5729
+ return notFound(reply);
5723
5730
  return reply.sendFile("index.html");
5724
5731
  });
5725
5732
  }
@@ -5758,11 +5765,11 @@ async function startServer(overrides = {}) {
5758
5765
 
5759
5766
  // src/watch.ts
5760
5767
  import { existsSync as existsSync9 } from "fs";
5761
- import { join as join12 } from "path";
5768
+ import { join as join10 } from "path";
5762
5769
  import chokidar from "chokidar";
5763
5770
  async function runWatch(opts = {}) {
5764
5771
  const sources = loadSources();
5765
- const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5772
+ const lockPath = join10(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5766
5773
  let running = false;
5767
5774
  let pending = false;
5768
5775
  const cycle = () => {
@@ -5795,7 +5802,7 @@ async function runWatch(opts = {}) {
5795
5802
  }
5796
5803
  };
5797
5804
  cycle();
5798
- const watchPaths = sources.map((s) => join12(s.configDir, "projects")).filter((p) => existsSync9(p));
5805
+ const watchPaths = sources.map((s) => join10(s.configDir, "projects")).filter((p) => existsSync9(p));
5799
5806
  if (watchPaths.length === 0) {
5800
5807
  console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
5801
5808
  }
@@ -5823,14 +5830,14 @@ async function runWatch(opts = {}) {
5823
5830
  // src/index.ts
5824
5831
  function version() {
5825
5832
  try {
5826
- const pkg = JSON.parse(readFileSync8(join13(dirname7(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
5833
+ const pkg = JSON.parse(readFileSync6(join11(dirname7(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
5827
5834
  return pkg.version ?? "0.0.0";
5828
5835
  } catch {
5829
5836
  return "0.0.0";
5830
5837
  }
5831
5838
  }
5832
5839
  function withLock(fn) {
5833
- const lockPath = join13(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5840
+ const lockPath = join11(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
5834
5841
  const lock = acquireLock(lockPath);
5835
5842
  if (!lock) {
5836
5843
  console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");