@swestash/agent-lens 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -3
- package/dist/agent-lens.js +157 -21
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/{CopyButton-hsiW2B6o.js → CopyButton-XWWtxa5T.js} +1 -1
- package/web/assets/{Dashboard-CPfpfHm8.js → Dashboard-BQX66iZp.js} +34 -34
- package/web/assets/{SecurityView-CvWWSbuL.js → SecurityView-C_CfNLoD.js} +1 -1
- package/web/assets/{SessionView-BLTA0iKW.js → SessionView-qPYTooYU.js} +8 -8
- package/web/assets/{SkillView-DVoLJAU0.js → SkillView-BuWdX4As.js} +1 -1
- package/web/assets/{SkillsView-c6k5JsWR.js → SkillsView-D8Wtsxi6.js} +1 -1
- package/web/assets/{WorkflowView-juGHkf05.js → WorkflowView-Afsv_IWs.js} +1 -1
- package/web/assets/index-BkDuOrzs.js +68 -0
- package/web/assets/index-CPyBa1Te.css +1 -0
- package/web/assets/{index-BXp9oWUn.js → index-T6SPgI6b.js} +1 -1
- package/web/assets/{severity-ZaGgvxL_.js → severity-DkldRG9Z.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-BfFCjgfs.css +0 -1
- package/web/assets/index-D-kuW17t.js +0 -68
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agent Lens
|
|
2
2
|
|
|
3
|
-
> Passively collect,
|
|
3
|
+
> Passively collect, search, and audit your Claude Code CLI session traces — **100% local**.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@swestash/agent-lens)
|
|
6
6
|

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