@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.
- package/dist/agent-lens.js +829 -822
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/404.html +25 -0
- package/web/assets/Dashboard-BkRLAY2N.js +69 -0
- package/web/assets/FileView-BbPK6hx9.js +1 -0
- package/web/assets/FilesView-D0gm4_XO.js +1 -0
- package/web/assets/SecurityView-92YGEb3-.js +1 -0
- package/web/assets/SessionView-Dy0ELjaS.js +12 -0
- package/web/assets/SkillView-Te5jcVBw.js +1 -0
- package/web/assets/SkillsView-FtrADGqR.js +1 -0
- package/web/assets/WorkflowView-D45UzOuV.js +1 -0
- package/web/assets/{index-B4Q15rek.js → index-Bgxu3c84.js} +1 -1
- package/web/assets/{index-Dt08Kre7.css → index-DBaa9asK.css} +1 -1
- package/web/assets/index-DYY0_SmQ.js +69 -0
- package/web/assets/jsonish-Dd3X-MG6.js +1 -0
- package/web/assets/{severity-CFSW4Y1W.js → severity-CoO1IKlD.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/CopyButton-DiThqN-q.js +0 -1
- package/web/assets/Dashboard-DFGNblG0.js +0 -69
- package/web/assets/FileView-DdnRD2n2.js +0 -1
- package/web/assets/FilesView-C27wIUJg.js +0 -1
- package/web/assets/SecurityView-DJXFERPR.js +0 -1
- package/web/assets/SessionView-CKQYkqQ6.js +0 -12
- package/web/assets/SkillView-Drf3CfEj.js +0 -1
- package/web/assets/SkillsView-bpP5ugsl.js +0 -1
- package/web/assets/WorkflowView-D-imBkEQ.js +0 -1
- package/web/assets/index-CaRPHep5.js +0 -69
package/dist/agent-lens.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { existsSync as existsSync10, readFileSync as
|
|
5
|
-
import { dirname as dirname7, join as
|
|
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:
|
|
976
|
-
{ name: "home-path-with-user", re:
|
|
977
|
-
{ name: "ipv4", re:
|
|
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:
|
|
1006
|
-
{ re:
|
|
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:
|
|
1026
|
-
{ name: "home-path-with-user", re:
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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"
|
|
1221
|
-
{ re: /\bBlocked:/i, type: "guardrail-blocked"
|
|
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"
|
|
1224
|
-
{ re: /exceeds maximum allowed tokens/i, type: "token-limit"
|
|
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"
|
|
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"
|
|
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.
|
|
1239
|
+
return { type: p.type, kind: errorKind(p.type) };
|
|
1237
1240
|
}
|
|
1238
|
-
return { type: "other", kind: "
|
|
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 =
|
|
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],
|
|
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],
|
|
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
|
|
2081
|
-
import { dirname as dirname5, join as
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
2422
|
-
|
|
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
|
-
|
|
2445
|
-
|
|
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
|
|
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
|
|
2473
|
-
const docFiles = s.loc.files > 0 ? countMatch(
|
|
2474
|
-
const opsFiles = s.loc.files > 0 ? countMatch(
|
|
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(
|
|
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 (
|
|
2518
|
-
db
|
|
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
|
|
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
|
-
|
|
2781
|
-
|
|
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
|
-
|
|
2828
|
-
|
|
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 (
|
|
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
|
-
|
|
2951
|
-
|
|
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
|
-
|
|
2968
|
-
|
|
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
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
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
|
-
|
|
3078
|
-
|
|
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
|
-
|
|
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
|
|
3121
|
-
const
|
|
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 (
|
|
3128
|
-
db
|
|
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 (
|
|
3205
|
-
db
|
|
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.
|
|
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
|
|
3547
|
-
|
|
3548
|
-
const
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
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
|
-
|
|
3588
|
-
|
|
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
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
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
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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/
|
|
3930
|
-
function
|
|
3921
|
+
// ../ingest/dist/sidecar.js
|
|
3922
|
+
function newSidecarStats() {
|
|
3931
3923
|
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
3932
3924
|
}
|
|
3933
|
-
|
|
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
|
-
|
|
3944
|
-
else if (e.isFile() &&
|
|
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
|
|
3947
|
+
function discover(sourceArchiveDir, subdir, matchName) {
|
|
3949
3948
|
const out = [];
|
|
3950
|
-
|
|
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
|
-
|
|
3953
|
+
walk(join7(sourceArchiveDir, ts.name, "projects"), subdir, false, matchName, out);
|
|
3955
3954
|
} catch {
|
|
3956
3955
|
}
|
|
3957
3956
|
return out;
|
|
3958
3957
|
}
|
|
3959
|
-
function
|
|
3960
|
-
|
|
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
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
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/
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
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
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
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
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
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
|
-
(@
|
|
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
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
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
|
-
|
|
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 {
|
|
4171
|
-
|
|
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
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
4340
|
-
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash:
|
|
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,
|
|
4344
|
-
ingestSubagentMeta(db,
|
|
4345
|
-
ingestToolResults(db,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4564
|
-
|
|
4565
|
-
|
|
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
|
|
4580
|
-
|
|
4581
|
-
r.
|
|
4582
|
-
r.
|
|
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
|
|
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
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
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
|
|
4604
|
+
pushGrouped(toolsByEvent, t.event_uuid, t);
|
|
4642
4605
|
}
|
|
4643
|
-
|
|
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
|
-
|
|
4659
|
-
|
|
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
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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
|
|
4708
|
-
FROM sessions s ${hasMeta
|
|
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
|
|
4713
|
-
|
|
4714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
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
|
|
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
|
|
4769
|
-
FROM sessions s ${hasMeta
|
|
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
|
|
4778
|
-
|
|
4779
|
-
|
|
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
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
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(
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
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
|
|
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 ${
|
|
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
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
5304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5361
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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:
|
|
5594
|
-
offset:
|
|
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
|
|
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
|
|
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:
|
|
5630
|
-
offset:
|
|
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
|
-
|
|
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
|
|
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:
|
|
5698
|
-
offset:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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) =>
|
|
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(
|
|
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 =
|
|
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");
|