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