@swestash/agent-lens 0.9.3 → 0.9.5
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 +271 -239
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/404.html +25 -0
- package/web/assets/Dashboard-BkRLAY2N.js +69 -0
- package/web/assets/{FileView-CJ6k5gBT.js → FileView-BbPK6hx9.js} +1 -1
- package/web/assets/{FilesView-ZB8kU8Hl.js → FilesView-D0gm4_XO.js} +1 -1
- package/web/assets/SecurityView-92YGEb3-.js +1 -0
- package/web/assets/{SessionView-BsrumXE9.js → SessionView-Dy0ELjaS.js} +8 -8
- package/web/assets/{SkillView-DhhjbXCQ.js → SkillView-Te5jcVBw.js} +1 -1
- package/web/assets/{SkillsView-CXjGf2j5.js → SkillsView-FtrADGqR.js} +1 -1
- package/web/assets/{WorkflowView-DVgrrFly.js → WorkflowView-D45UzOuV.js} +1 -1
- package/web/assets/{index-DumYdTnf.js → index-Bgxu3c84.js} +1 -1
- package/web/assets/{index-Dt08Kre7.css → index-DBaa9asK.css} +1 -1
- package/web/assets/{index-F_4kZQVd.js → index-DYY0_SmQ.js} +2 -2
- package/web/assets/jsonish-Dd3X-MG6.js +1 -0
- package/web/assets/{severity-ChuAKM2D.js → severity-CoO1IKlD.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/Dashboard-BeHGt7nW.js +0 -69
- package/web/assets/SecurityView-DOUnL_jp.js +0 -1
- package/web/assets/jsonish-B1rzsNYu.js +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -2424,7 +2424,7 @@ function parseStored(json, where) {
|
|
|
2424
2424
|
}
|
|
2425
2425
|
|
|
2426
2426
|
// ../ingest/dist/classify.js
|
|
2427
|
-
var CLASSIFIER_VERSION =
|
|
2427
|
+
var CLASSIFIER_VERSION = 3;
|
|
2428
2428
|
var CATEGORIES = ["bugfix", "refactor", "docs", "ops", "review", "feature", "chore"];
|
|
2429
2429
|
var BANDS = [
|
|
2430
2430
|
{ max: 22, band: "trivial" },
|
|
@@ -2467,14 +2467,23 @@ function band(score) {
|
|
|
2467
2467
|
return "xl";
|
|
2468
2468
|
}
|
|
2469
2469
|
var clamp01 = (x) => x < 0 ? 0 : x > 1 ? 1 : x;
|
|
2470
|
+
var WORD_RE = /* @__PURE__ */ new Map();
|
|
2471
|
+
function wordRe(phrase) {
|
|
2472
|
+
let re = WORD_RE.get(phrase);
|
|
2473
|
+
if (!re) {
|
|
2474
|
+
const escaped = phrase.trim().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2475
|
+
re = new RegExp(`\\b${escaped}\\b`, "g");
|
|
2476
|
+
WORD_RE.set(phrase, re);
|
|
2477
|
+
}
|
|
2478
|
+
return re;
|
|
2479
|
+
}
|
|
2470
2480
|
function hits(text, words) {
|
|
2471
2481
|
let n = 0;
|
|
2472
2482
|
for (const w of words) {
|
|
2473
|
-
|
|
2474
|
-
|
|
2483
|
+
const re = wordRe(w);
|
|
2484
|
+
re.lastIndex = 0;
|
|
2485
|
+
while (re.exec(text) !== null)
|
|
2475
2486
|
n++;
|
|
2476
|
-
i = text.indexOf(w, i + w.length);
|
|
2477
|
-
}
|
|
2478
2487
|
}
|
|
2479
2488
|
return n;
|
|
2480
2489
|
}
|
|
@@ -2496,9 +2505,9 @@ function scoreCategories(s, text, files) {
|
|
|
2496
2505
|
scores.bugfix += hits(t, ["bug", "fix", "broken", "crash", "regression", "traceback", "stack trace", "failing", "error", "exception", "not working"]) * 1;
|
|
2497
2506
|
scores.refactor += hits(t, ["refactor", "clean up", "cleanup", "rename", "extract", "simplify", "dead code", "tidy", "reorganize", "restructure"]) * 1;
|
|
2498
2507
|
scores.docs += hits(t, ["readme", "document", "documentation", "changelog", "docstring", "comment", "write docs", "usage guide"]) * 1;
|
|
2499
|
-
scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci
|
|
2508
|
+
scores.ops += hits(t, ["deploy", "docker", "pipeline", "ci/cd", "ci", "systemd", "infra", "kubernetes", "terraform", "release", "rollback", "container"]) * 1;
|
|
2500
2509
|
scores.review += hits(t, ["review", "audit", "inspect", "look over", "feedback on"]) * 1;
|
|
2501
|
-
scores.feature += hits(t, ["add
|
|
2510
|
+
scores.feature += hits(t, ["add", "implement", "build", "create", "feature", "support for", "new feature"]) * 0.8;
|
|
2502
2511
|
const docFiles = s.loc.files > 0 ? countMatch(files, DOC_EXT) : 0;
|
|
2503
2512
|
const opsFiles = s.loc.files > 0 ? countMatch(files, OPS_FILE) : 0;
|
|
2504
2513
|
if (s.loc.files > 0) {
|
|
@@ -4305,6 +4314,9 @@ function tableExists(db, name) {
|
|
|
4305
4314
|
function queryAll(db, sql, ...params) {
|
|
4306
4315
|
return db.prepare(sql).all(...params);
|
|
4307
4316
|
}
|
|
4317
|
+
function queryGet(db, sql, ...params) {
|
|
4318
|
+
return db.prepare(sql).get(...params);
|
|
4319
|
+
}
|
|
4308
4320
|
function pushDateRange(clauses, params, col, from, to) {
|
|
4309
4321
|
if (from) {
|
|
4310
4322
|
clauses.push(`date(${col}) >= date(?)`);
|
|
@@ -4325,6 +4337,16 @@ function orderBy(columns, sort, fallback, dir) {
|
|
|
4325
4337
|
const col = sort !== void 0 && Object.hasOwn(columns, sort) ? columns[sort] : columns[fallback];
|
|
4326
4338
|
return `${col} ${dir === "asc" ? "ASC" : "DESC"}`;
|
|
4327
4339
|
}
|
|
4340
|
+
function pageLimit(raw, fallback, max) {
|
|
4341
|
+
const n = Math.floor(Number(raw));
|
|
4342
|
+
if (!Number.isFinite(n) || n < 1)
|
|
4343
|
+
return Math.min(fallback, max);
|
|
4344
|
+
return Math.min(n, max);
|
|
4345
|
+
}
|
|
4346
|
+
function pageOffset(raw) {
|
|
4347
|
+
const n = Math.floor(Number(raw));
|
|
4348
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
4349
|
+
}
|
|
4328
4350
|
function pushGrouped(map, key, value) {
|
|
4329
4351
|
const list = map.get(key);
|
|
4330
4352
|
if (list)
|
|
@@ -4400,22 +4422,17 @@ function extractParts(rawJson) {
|
|
|
4400
4422
|
return { text: text || null, thinking: thinking || null };
|
|
4401
4423
|
}
|
|
4402
4424
|
function listSources(db) {
|
|
4403
|
-
return db.
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
// many subagent sidechains each task spawns.
|
|
4407
|
-
`SELECT s.id, s.label, s.agent_id, s.config_dir,
|
|
4408
|
-
(SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
|
|
4409
|
-
FROM sources s ORDER BY s.label`
|
|
4410
|
-
).all();
|
|
4425
|
+
return queryAll(db, `SELECT s.id, s.label,
|
|
4426
|
+
(SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
|
|
4427
|
+
FROM sources s ORDER BY s.label`);
|
|
4411
4428
|
}
|
|
4412
4429
|
function listProjects(db) {
|
|
4413
|
-
return db
|
|
4414
|
-
|
|
4415
|
-
|
|
4430
|
+
return queryAll(db, `SELECT p.id, p.path,
|
|
4431
|
+
(SELECT COUNT(*) FROM sessions x WHERE x.project_id = p.id AND x.is_sidechain = 0) AS session_count
|
|
4432
|
+
FROM projects p ORDER BY session_count DESC, p.path`);
|
|
4416
4433
|
}
|
|
4417
4434
|
function listModels(db) {
|
|
4418
|
-
return db
|
|
4435
|
+
return queryAll(db, `SELECT DISTINCT model FROM token_usage WHERE model IS NOT NULL ORDER BY model`).map((r) => r.model);
|
|
4419
4436
|
}
|
|
4420
4437
|
function toFtsQuery(raw) {
|
|
4421
4438
|
return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
|
|
@@ -4452,8 +4469,8 @@ function listSessions(db, f) {
|
|
|
4452
4469
|
params.push(toFtsQuery(f.q), like, like, like);
|
|
4453
4470
|
}
|
|
4454
4471
|
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
4455
|
-
const total = db
|
|
4456
|
-
const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,
|
|
4472
|
+
const total = queryGet(db, `SELECT COUNT(*) n FROM sessions s ${whereSql}`, ...params).n;
|
|
4473
|
+
const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,
|
|
4457
4474
|
s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
|
|
4458
4475
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
|
|
4459
4476
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
|
|
@@ -4472,10 +4489,9 @@ function listSessions(db, f) {
|
|
|
4472
4489
|
};
|
|
4473
4490
|
const sortKey = f.sort ?? "started";
|
|
4474
4491
|
const JS_SORT = /* @__PURE__ */ new Set(["tokens", "cost", "errors", "security"]);
|
|
4475
|
-
let
|
|
4492
|
+
let sessions;
|
|
4476
4493
|
if (JS_SORT.has(sortKey)) {
|
|
4477
|
-
const all = db
|
|
4478
|
-
attachSessionCost(db, all);
|
|
4494
|
+
const all = attachSessionCost(db, queryAll(db, baseSelect, ...params));
|
|
4479
4495
|
const sign = f.dir === "asc" ? 1 : -1;
|
|
4480
4496
|
const metric = (r) => {
|
|
4481
4497
|
switch (sortKey) {
|
|
@@ -4496,12 +4512,11 @@ function listSessions(db, f) {
|
|
|
4496
4512
|
const c = (metric(a) - metric(b)) * sign;
|
|
4497
4513
|
return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
|
|
4498
4514
|
});
|
|
4499
|
-
|
|
4515
|
+
sessions = all.slice(f.offset, f.offset + f.limit);
|
|
4500
4516
|
} else {
|
|
4501
|
-
|
|
4502
|
-
attachSessionCost(db, rows);
|
|
4517
|
+
sessions = attachSessionCost(db, queryAll(db, `${baseSelect} ORDER BY ${orderBy(NATIVE_ORDER, sortKey, "started", f.dir)}, s.id ASC LIMIT ? OFFSET ?`, ...params, f.limit, f.offset));
|
|
4503
4518
|
}
|
|
4504
|
-
return { total, sessions
|
|
4519
|
+
return { total, sessions };
|
|
4505
4520
|
}
|
|
4506
4521
|
function attachSessionCost(db, rows) {
|
|
4507
4522
|
const usageBySession = /* @__PURE__ */ new Map();
|
|
@@ -4517,21 +4532,18 @@ function attachSessionCost(db, rows) {
|
|
|
4517
4532
|
usageBySession.set(u.session_id, list);
|
|
4518
4533
|
}
|
|
4519
4534
|
}
|
|
4520
|
-
|
|
4535
|
+
return rows.map(({ ai_title, slug, ...r }) => {
|
|
4521
4536
|
const u = usageBySession.get(r.id) ?? [];
|
|
4522
4537
|
const split = splitOf(u);
|
|
4523
|
-
r
|
|
4524
|
-
|
|
4525
|
-
r.cost = costOf(u);
|
|
4526
|
-
r.title = r.ai_title || r.slug || null;
|
|
4527
|
-
}
|
|
4538
|
+
return { ...r, tokens: tokensOf(split), token_split: split, cost: costOf(u), title: ai_title || slug || null };
|
|
4539
|
+
});
|
|
4528
4540
|
}
|
|
4529
4541
|
function loadToolCalls(db, id) {
|
|
4530
|
-
const toolRows = db
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4542
|
+
const toolRows = queryAll(db, `SELECT id, event_uuid, tool_name, skill_name, skill_id, agent_type, spawned_session_id,
|
|
4543
|
+
workflow_run_id, workflow_name, status, error_type,
|
|
4544
|
+
total_duration_ms, input_json, result_summary,
|
|
4545
|
+
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
|
|
4546
|
+
FROM tool_calls WHERE session_id = ?`, id);
|
|
4535
4547
|
if (tableExists(db, "tool_results")) {
|
|
4536
4548
|
const getFull = db.prepare("SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?");
|
|
4537
4549
|
for (const t of toolRows) {
|
|
@@ -4549,8 +4561,8 @@ function loadFindings(db, id, toolRows) {
|
|
|
4549
4561
|
if (!tableExists(db, "findings"))
|
|
4550
4562
|
return [];
|
|
4551
4563
|
const sessionFindings = [];
|
|
4552
|
-
const findingRows = db
|
|
4553
|
-
|
|
4564
|
+
const findingRows = queryAll(db, `SELECT id, tool_call_id, event_uuid, turn_id, rule_id, category, framework_ref, severity, title, evidence, signals_json
|
|
4565
|
+
FROM findings WHERE session_id = ?`, id);
|
|
4554
4566
|
const byToolCall = /* @__PURE__ */ new Map();
|
|
4555
4567
|
for (const fr of findingRows) {
|
|
4556
4568
|
const f = {
|
|
@@ -4564,6 +4576,7 @@ function loadFindings(db, id, toolRows) {
|
|
|
4564
4576
|
severity: fr.severity,
|
|
4565
4577
|
title: fr.title,
|
|
4566
4578
|
evidence: fr.evidence,
|
|
4579
|
+
// Asserted, not validated: safeJson returns unknown and this is the assertion (ADR-026).
|
|
4567
4580
|
signals: safeJson(fr.signals_json)
|
|
4568
4581
|
};
|
|
4569
4582
|
sessionFindings.push(f);
|
|
@@ -4571,13 +4584,16 @@ function loadFindings(db, id, toolRows) {
|
|
|
4571
4584
|
pushGrouped(byToolCall, fr.tool_call_id, f);
|
|
4572
4585
|
}
|
|
4573
4586
|
for (const t of toolRows)
|
|
4574
|
-
t.findings = byToolCall.get(t.id) ?? [];
|
|
4587
|
+
t.findings = t.id ? byToolCall.get(t.id) ?? [] : [];
|
|
4575
4588
|
sessionFindings.sort((a, b) => severityRank(b.severity) - severityRank(a.severity));
|
|
4576
4589
|
return sessionFindings;
|
|
4577
4590
|
}
|
|
4591
|
+
function toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }) {
|
|
4592
|
+
return call;
|
|
4593
|
+
}
|
|
4578
4594
|
function loadEvents(db, id, toolRows) {
|
|
4579
|
-
const eventRows = db
|
|
4580
|
-
|
|
4595
|
+
const eventRows = queryAll(db, `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
|
|
4596
|
+
FROM events WHERE session_id = ? ORDER BY timestamp, seq`, id);
|
|
4581
4597
|
const toolsByEvent = /* @__PURE__ */ new Map();
|
|
4582
4598
|
for (const t of toolRows) {
|
|
4583
4599
|
if (!t.event_uuid)
|
|
@@ -4596,13 +4612,13 @@ function loadEvents(db, id, toolRows) {
|
|
|
4596
4612
|
turn_id: e.turn_id,
|
|
4597
4613
|
text,
|
|
4598
4614
|
thinking,
|
|
4599
|
-
toolCalls: toolsByEvent.get(e.uuid) ?? []
|
|
4615
|
+
toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall)
|
|
4600
4616
|
};
|
|
4601
4617
|
});
|
|
4602
4618
|
}
|
|
4603
4619
|
function loadClassification(db, id) {
|
|
4604
|
-
const cls = db
|
|
4605
|
-
|
|
4620
|
+
const cls = queryGet(db, `SELECT category, complexity_score, complexity_band, signals_json, classifier_version
|
|
4621
|
+
FROM classifications WHERE scope = 'session' AND target_id = ?`, id);
|
|
4606
4622
|
if (!cls)
|
|
4607
4623
|
return null;
|
|
4608
4624
|
let signals = null;
|
|
@@ -4621,66 +4637,67 @@ function loadClassification(db, id) {
|
|
|
4621
4637
|
function loadParent(db, session) {
|
|
4622
4638
|
if (!session.parent_session_id)
|
|
4623
4639
|
return null;
|
|
4624
|
-
const p = db
|
|
4625
|
-
|
|
4626
|
-
|
|
4640
|
+
const p = queryGet(db, `SELECT ps.id, ps.ai_title, ps.slug, t.seq AS turn_seq
|
|
4641
|
+
FROM sessions ps LEFT JOIN turns t ON t.id = ?
|
|
4642
|
+
WHERE ps.id = ?`, session.parent_turn_id, session.parent_session_id);
|
|
4627
4643
|
return p ? { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null } : null;
|
|
4628
4644
|
}
|
|
4629
4645
|
function loadChildren(db, id) {
|
|
4630
4646
|
const hasMeta = tableExists(db, "session_meta");
|
|
4631
|
-
const children = db
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
ch.title = ch.ai_title || ch.slug || null;
|
|
4647
|
+
const children = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
|
|
4648
|
+
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4649
|
+
${metaProjection(hasMeta)}
|
|
4650
|
+
FROM sessions s ${metaJoin(hasMeta)}
|
|
4651
|
+
WHERE s.parent_session_id = ? ORDER BY s.started_at`, id);
|
|
4652
|
+
return children.map(({ ai_title, slug, ...ch }) => {
|
|
4638
4653
|
const u = sessionUsage(db, ch.id);
|
|
4639
|
-
ch
|
|
4640
|
-
|
|
4641
|
-
}
|
|
4642
|
-
return children;
|
|
4654
|
+
return { ...ch, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };
|
|
4655
|
+
});
|
|
4643
4656
|
}
|
|
4644
4657
|
function loadWorkflowRuns(db, id) {
|
|
4645
4658
|
const hasWR = tableExists(db, "workflow_results");
|
|
4646
|
-
return db
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4659
|
+
return queryAll(db, `SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
|
|
4660
|
+
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
|
|
4661
|
+
${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
|
|
4662
|
+
FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
|
|
4663
|
+
WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
|
|
4664
|
+
ORDER BY t.seq`, id);
|
|
4652
4665
|
}
|
|
4653
4666
|
function loadFileChanges(db, id) {
|
|
4654
4667
|
if (!tableExists(db, "file_changes"))
|
|
4655
4668
|
return [];
|
|
4656
|
-
return db
|
|
4657
|
-
|
|
4658
|
-
|
|
4669
|
+
return queryAll(db, `SELECT fc.id, fc.tool_call_id, fc.turn_id, fc.event_uuid, fc.file_path, fc.tool_name,
|
|
4670
|
+
fc.lines_added, fc.lines_removed, fc.timestamp
|
|
4671
|
+
FROM file_changes fc WHERE fc.session_id = ? ORDER BY fc.timestamp`, id);
|
|
4659
4672
|
}
|
|
4660
|
-
function
|
|
4673
|
+
function withSessionTotals(db, session, id, toolRows) {
|
|
4661
4674
|
const usage = sessionUsage(db, id);
|
|
4662
4675
|
const split = splitOf(usage);
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4676
|
+
const tool_error_count = toolRows.filter((t) => t.status === "error").length;
|
|
4677
|
+
const tool_rejection_count = toolRows.filter((t) => t.status === "error" && t.error_type && errorKind(t.error_type) === "rejection").length;
|
|
4678
|
+
return {
|
|
4679
|
+
...session,
|
|
4680
|
+
tokens: tokensOf(split),
|
|
4681
|
+
token_split: split,
|
|
4682
|
+
cost: costOf(usage),
|
|
4683
|
+
title: session.ai_title || session.slug || null,
|
|
4684
|
+
tool_call_count: toolRows.length,
|
|
4685
|
+
tool_error_count,
|
|
4686
|
+
tool_rejection_count,
|
|
4687
|
+
tool_failure_count: tool_error_count - tool_rejection_count
|
|
4688
|
+
};
|
|
4671
4689
|
}
|
|
4672
4690
|
function getSession(db, id) {
|
|
4673
|
-
const session = db
|
|
4674
|
-
|
|
4691
|
+
const session = queryGet(db, `SELECT s.*, p.path AS project_path FROM sessions s
|
|
4692
|
+
LEFT JOIN projects p ON p.id = s.project_id WHERE s.id = ?`, id);
|
|
4675
4693
|
if (!session)
|
|
4676
4694
|
return null;
|
|
4677
|
-
const turns = db
|
|
4695
|
+
const turns = queryAll(db, "SELECT * FROM turns WHERE session_id = ? ORDER BY seq", id);
|
|
4678
4696
|
const toolRows = loadToolCalls(db, id);
|
|
4679
4697
|
const findings = loadFindings(db, id, toolRows);
|
|
4680
4698
|
const events = loadEvents(db, id, toolRows);
|
|
4681
|
-
attachSessionTotals(db, session, id, toolRows);
|
|
4682
4699
|
return {
|
|
4683
|
-
session,
|
|
4700
|
+
session: withSessionTotals(db, session, id, toolRows),
|
|
4684
4701
|
turns,
|
|
4685
4702
|
events,
|
|
4686
4703
|
classification: loadClassification(db, id),
|
|
@@ -4705,27 +4722,26 @@ function safeJson(s) {
|
|
|
4705
4722
|
}
|
|
4706
4723
|
function loadAgents(db, runId) {
|
|
4707
4724
|
const hasMeta = tableExists(db, "session_meta");
|
|
4708
|
-
const
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4725
|
+
const agentRows = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
|
|
4726
|
+
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4727
|
+
${metaProjection(hasMeta)}
|
|
4728
|
+
FROM sessions s ${metaJoin(hasMeta)}
|
|
4729
|
+
WHERE s.workflow_run_id = ? ORDER BY s.started_at`, runId);
|
|
4713
4730
|
let total_tokens = 0;
|
|
4714
4731
|
let total_cost = 0;
|
|
4715
4732
|
let started_at = null;
|
|
4716
4733
|
let ended_at = null;
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
const
|
|
4720
|
-
a.tokens = tokensOf(splitOf(u));
|
|
4721
|
-
a.cost = costOf(u);
|
|
4734
|
+
const agents = agentRows.map(({ ai_title, slug, ...r }) => {
|
|
4735
|
+
const u = sessionUsage(db, r.id);
|
|
4736
|
+
const a = { ...r, title: ai_title || slug || null, tokens: tokensOf(splitOf(u)), cost: costOf(u) };
|
|
4722
4737
|
total_tokens += a.tokens;
|
|
4723
4738
|
total_cost += a.cost;
|
|
4724
4739
|
if (a.started_at && (!started_at || a.started_at < started_at))
|
|
4725
4740
|
started_at = a.started_at;
|
|
4726
4741
|
if (a.ended_at && (!ended_at || a.ended_at > ended_at))
|
|
4727
4742
|
ended_at = a.ended_at;
|
|
4728
|
-
|
|
4743
|
+
return a;
|
|
4744
|
+
});
|
|
4729
4745
|
const duration_ms = started_at && ended_at ? new Date(ended_at).getTime() - new Date(started_at).getTime() : null;
|
|
4730
4746
|
return {
|
|
4731
4747
|
agents,
|
|
@@ -4735,9 +4751,9 @@ function loadAgents(db, runId) {
|
|
|
4735
4751
|
function parseCompletion(db, wf) {
|
|
4736
4752
|
if (!wf.tool_use_id || !wf.parent_session_id)
|
|
4737
4753
|
return null;
|
|
4738
|
-
const ev = db
|
|
4739
|
-
|
|
4740
|
-
|
|
4754
|
+
const ev = queryGet(db, `SELECT raw_json FROM events
|
|
4755
|
+
WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
|
|
4756
|
+
ORDER BY timestamp DESC LIMIT 1`, wf.parent_session_id, `%${wf.tool_use_id}%`);
|
|
4741
4757
|
if (!ev)
|
|
4742
4758
|
return null;
|
|
4743
4759
|
const { text } = extractParts(ev.raw_json);
|
|
@@ -4745,9 +4761,9 @@ function parseCompletion(db, wf) {
|
|
|
4745
4761
|
return { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
|
|
4746
4762
|
}
|
|
4747
4763
|
function loadRunSidecar(db, runId, completion) {
|
|
4748
|
-
const wr = tableExists(db, "workflow_results") ? db
|
|
4749
|
-
|
|
4750
|
-
|
|
4764
|
+
const wr = tableExists(db, "workflow_results") ? queryGet(db, `SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
|
|
4765
|
+
agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
|
|
4766
|
+
FROM workflow_results WHERE run_id = ?`, runId) ?? null : null;
|
|
4751
4767
|
if (!wr)
|
|
4752
4768
|
return { wr: null, run: null, completion };
|
|
4753
4769
|
const run = {
|
|
@@ -4760,6 +4776,8 @@ function loadRunSidecar(db, runId, completion) {
|
|
|
4760
4776
|
duration_ms: wr.duration_ms ?? null,
|
|
4761
4777
|
started_at: wr.started_at ?? null,
|
|
4762
4778
|
ended_at: wr.ended_at ?? null,
|
|
4779
|
+
// The three JSON blobs are asserted, not validated (ADR-026): safeJson yields null for malformed
|
|
4780
|
+
// stored text, and these casts are what claims the shape of everything else.
|
|
4763
4781
|
phases: safeJson(wr.phases_json),
|
|
4764
4782
|
logs: safeJson(wr.logs_json),
|
|
4765
4783
|
// The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
|
|
@@ -4777,16 +4795,16 @@ function loadRunSidecar(db, runId, completion) {
|
|
|
4777
4795
|
return { wr, run, completion };
|
|
4778
4796
|
}
|
|
4779
4797
|
function getWorkflow(db, runId) {
|
|
4780
|
-
const wf = db
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4798
|
+
const wf = queryGet(db, `SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,
|
|
4799
|
+
tc.input_json AS input_json,
|
|
4800
|
+
tc.session_id AS parent_session_id, t.seq AS turn_seq,
|
|
4801
|
+
ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
|
|
4802
|
+
FROM tool_calls tc
|
|
4803
|
+
LEFT JOIN turns t ON t.id = tc.turn_id
|
|
4804
|
+
LEFT JOIN sessions ps ON ps.id = tc.session_id
|
|
4805
|
+
WHERE tc.workflow_run_id = ? AND tc.tool_name = 'Workflow'
|
|
4806
|
+
ORDER BY t.seq
|
|
4807
|
+
LIMIT 1`, runId);
|
|
4790
4808
|
if (!wf)
|
|
4791
4809
|
return null;
|
|
4792
4810
|
const { agents, stats } = loadAgents(db, runId);
|
|
@@ -4816,20 +4834,18 @@ function listSkills(db, f = {}) {
|
|
|
4816
4834
|
where.push("s.source_id = ?"), params.push(f.source);
|
|
4817
4835
|
if (f.project)
|
|
4818
4836
|
where.push("s.project_id = ?"), params.push(f.project);
|
|
4819
|
-
const rows = db
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
r.sources = r.sources ? String(r.sources).split(",").filter(Boolean) : [];
|
|
4832
|
-
return rows;
|
|
4837
|
+
const rows = queryAll(db, `SELECT tc.skill_name AS name,
|
|
4838
|
+
COUNT(*) AS call_count,
|
|
4839
|
+
COUNT(DISTINCT tc.skill_id) AS version_count,
|
|
4840
|
+
MAX(e.timestamp) AS last_fired,
|
|
4841
|
+
GROUP_CONCAT(DISTINCT s.source_id) AS sources
|
|
4842
|
+
FROM tool_calls tc
|
|
4843
|
+
JOIN sessions s ON s.id = tc.session_id
|
|
4844
|
+
LEFT JOIN events e ON e.uuid = tc.event_uuid
|
|
4845
|
+
WHERE ${where.join(" AND ")}
|
|
4846
|
+
GROUP BY tc.skill_name
|
|
4847
|
+
ORDER BY call_count DESC, name`, ...params);
|
|
4848
|
+
return rows.map((r) => ({ ...r, sources: r.sources ? String(r.sources).split(",").filter(Boolean) : [] }));
|
|
4833
4849
|
}
|
|
4834
4850
|
function listFiles(db, f = {}) {
|
|
4835
4851
|
if (!tableExists(db, "file_changes"))
|
|
@@ -4852,7 +4868,7 @@ function listFiles(db, f = {}) {
|
|
|
4852
4868
|
MAX(fc.timestamp) AS last_ts
|
|
4853
4869
|
FROM file_changes fc${whereSql}
|
|
4854
4870
|
GROUP BY fc.project_id, fc.file_path`;
|
|
4855
|
-
const total = db
|
|
4871
|
+
const total = queryGet(db, `SELECT COUNT(*) n FROM (${grouped})`, ...params).n;
|
|
4856
4872
|
const ORDER = {
|
|
4857
4873
|
last_ts: "last_ts",
|
|
4858
4874
|
first_ts: "first_ts",
|
|
@@ -4862,9 +4878,9 @@ function listFiles(db, f = {}) {
|
|
|
4862
4878
|
};
|
|
4863
4879
|
const limit = Math.min(f.limit ?? 50, 200);
|
|
4864
4880
|
const offset = f.offset ?? 0;
|
|
4865
|
-
const files = db
|
|
4866
|
-
|
|
4867
|
-
|
|
4881
|
+
const files = queryAll(db, `SELECT g.*, p.path AS project_path FROM (${grouped}) g
|
|
4882
|
+
LEFT JOIN projects p ON p.id = g.project_id
|
|
4883
|
+
ORDER BY ${orderBy(ORDER, f.sort, "last_ts", f.dir)}, g.file_path ASC LIMIT ? OFFSET ?`, ...params, limit, offset);
|
|
4868
4884
|
return { total, files };
|
|
4869
4885
|
}
|
|
4870
4886
|
function getFileTimeline(db, filePath, projectId2) {
|
|
@@ -4874,15 +4890,15 @@ function getFileTimeline(db, filePath, projectId2) {
|
|
|
4874
4890
|
let scope = "fc.file_path = ?";
|
|
4875
4891
|
if (projectId2)
|
|
4876
4892
|
scope += " AND fc.project_id = ?", params.push(projectId2);
|
|
4877
|
-
const rows = db
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4893
|
+
const rows = queryAll(db, `SELECT fc.id, fc.session_id, fc.turn_id, fc.event_uuid, fc.tool_name, fc.lines_added,
|
|
4894
|
+
fc.lines_removed, fc.timestamp, fc.project_id,
|
|
4895
|
+
s.ai_title, s.slug, s.source_id, s.started_at, t.seq AS turn_seq, t.prompt_preview,
|
|
4896
|
+
p.path AS project_path
|
|
4897
|
+
FROM file_changes fc
|
|
4898
|
+
JOIN sessions s ON s.id = fc.session_id
|
|
4899
|
+
LEFT JOIN turns t ON t.id = fc.turn_id
|
|
4900
|
+
LEFT JOIN projects p ON p.id = fc.project_id
|
|
4901
|
+
WHERE ${scope} ORDER BY fc.timestamp`, ...params);
|
|
4886
4902
|
if (!rows.length)
|
|
4887
4903
|
return null;
|
|
4888
4904
|
const cat = tableExists(db, "classifications") ? db.prepare("SELECT category FROM classifications WHERE scope = 'session' AND target_id = ?") : null;
|
|
@@ -4931,22 +4947,21 @@ function getFileTimeline(db, filePath, projectId2) {
|
|
|
4931
4947
|
};
|
|
4932
4948
|
}
|
|
4933
4949
|
function getSkill(db, name) {
|
|
4934
|
-
const versions = db
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
const
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
if (!versions.length && !
|
|
4950
|
+
const versions = queryAll(db, `SELECT sk.id, sk.base_dir, sk.summary, sk.body, sk.body_bytes, sk.first_seen, sk.last_seen,
|
|
4951
|
+
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.skill_id = sk.id) AS call_count
|
|
4952
|
+
FROM skills sk WHERE sk.name = ? ORDER BY sk.last_seen DESC, sk.id`, name);
|
|
4953
|
+
const sessionRows = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.source_id, s.started_at, p.path AS project_path,
|
|
4954
|
+
tc.skill_id AS version_id, MIN(e.timestamp) AS fired_at, COUNT(*) AS fire_count
|
|
4955
|
+
FROM tool_calls tc
|
|
4956
|
+
JOIN sessions s ON s.id = tc.session_id
|
|
4957
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
4958
|
+
LEFT JOIN events e ON e.uuid = tc.event_uuid
|
|
4959
|
+
WHERE tc.tool_name = 'Skill' AND tc.skill_name = ?
|
|
4960
|
+
GROUP BY s.id, tc.skill_id
|
|
4961
|
+
ORDER BY fired_at DESC`, name);
|
|
4962
|
+
if (!versions.length && !sessionRows.length)
|
|
4947
4963
|
return null;
|
|
4948
|
-
|
|
4949
|
-
s.title = s.ai_title || s.slug || null;
|
|
4964
|
+
const sessions = sessionRows.map(({ ai_title, slug, ...s }) => ({ ...s, title: ai_title || slug || null }));
|
|
4950
4965
|
const call_count = sessions.reduce((a, s) => a + s.fire_count, 0);
|
|
4951
4966
|
return { name, versions, sessions, call_count };
|
|
4952
4967
|
}
|
|
@@ -4998,7 +5013,7 @@ function listFindings(db, f) {
|
|
|
4998
5013
|
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
4999
5014
|
const dismissJoin = attached ? "LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id" : "";
|
|
5000
5015
|
const triageCols = attached ? `, (d.finding_id IS NOT NULL) AS dismissed, d.note AS dismiss_note, d.dismissed_at, ${MUTED_SQL} AS muted` : `, 0 AS dismissed, NULL AS dismiss_note, NULL AS dismissed_at, 0 AS muted`;
|
|
5001
|
-
const total = db
|
|
5016
|
+
const total = queryGet(db, `SELECT COUNT(*) n FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin} ${whereSql}`, ...w.params).n;
|
|
5002
5017
|
const ORDER = {
|
|
5003
5018
|
severity: SEVERITY_RANK_SQL,
|
|
5004
5019
|
session: "f.session_id",
|
|
@@ -5006,18 +5021,18 @@ function listFindings(db, f) {
|
|
|
5006
5021
|
category: "f.category",
|
|
5007
5022
|
time: "s.started_at"
|
|
5008
5023
|
};
|
|
5009
|
-
const findings = db
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5024
|
+
const findings = queryAll(db, `SELECT f.id, f.session_id, f.tool_call_id, f.event_uuid, f.turn_id, f.rule_id, f.category,
|
|
5025
|
+
f.framework_ref, f.severity, f.title, f.evidence, tc.tool_name,
|
|
5026
|
+
COALESCE(s.ai_title, s.slug) AS session_title, s.source_id, s.is_sidechain,
|
|
5027
|
+
s.started_at, p.path AS project_path, s.project_id ${triageCols}
|
|
5028
|
+
FROM findings f
|
|
5029
|
+
JOIN sessions s ON s.id = f.session_id
|
|
5030
|
+
LEFT JOIN projects p ON p.id = s.project_id
|
|
5031
|
+
LEFT JOIN tool_calls tc ON tc.id = f.tool_call_id
|
|
5032
|
+
${dismissJoin}
|
|
5033
|
+
${whereSql}
|
|
5034
|
+
ORDER BY ${orderBy(ORDER, f.sort, "severity", f.dir)}, ${SEVERITY_RANK_SQL} DESC, f.id ASC
|
|
5035
|
+
LIMIT ? OFFSET ?`, ...w.params, f.limit, f.offset);
|
|
5021
5036
|
return { total, findings };
|
|
5022
5037
|
}
|
|
5023
5038
|
function securitySummary(db) {
|
|
@@ -5029,15 +5044,15 @@ function securitySummary(db) {
|
|
|
5029
5044
|
const dismissJoin = attached ? "LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id" : "";
|
|
5030
5045
|
const base = `FROM findings f JOIN sessions s ON s.id = f.session_id ${dismissJoin}`;
|
|
5031
5046
|
const openWhere = attached ? `WHERE d.finding_id IS NULL AND NOT ${MUTED_SQL}` : "";
|
|
5032
|
-
const scalar = (sql) => db
|
|
5047
|
+
const scalar = (sql) => queryGet(db, sql).n;
|
|
5033
5048
|
const total = scalar(`SELECT COUNT(*) n ${base} ${openWhere}`);
|
|
5034
5049
|
const sessions_flagged = scalar(`SELECT COUNT(DISTINCT f.session_id) n ${base} ${openWhere}`);
|
|
5035
5050
|
const dismissed = attached ? scalar(`SELECT COUNT(*) n FROM findings f LEFT JOIN triage.dismissed_findings d ON d.finding_id = f.id WHERE d.finding_id IS NOT NULL`) : 0;
|
|
5036
5051
|
const muted = attached ? scalar(`SELECT COUNT(*) n ${base} WHERE d.finding_id IS NULL AND ${MUTED_SQL}`) : 0;
|
|
5037
|
-
const by_severity = db
|
|
5038
|
-
const by_category = db
|
|
5039
|
-
const by_rule = db
|
|
5040
|
-
|
|
5052
|
+
const by_severity = queryAll(db, `SELECT f.severity AS severity, COUNT(*) n ${base} ${openWhere} GROUP BY f.severity ORDER BY ${SEVERITY_RANK_SQL} DESC`);
|
|
5053
|
+
const by_category = queryAll(db, `SELECT f.category AS category, COUNT(*) n ${base} ${openWhere} GROUP BY f.category ORDER BY n DESC`);
|
|
5054
|
+
const by_rule = queryAll(db, `SELECT f.rule_id AS rule_id, f.category AS category, f.title AS title, COUNT(*) n, MAX(${SEVERITY_RANK_SQL}) AS rank
|
|
5055
|
+
${base} ${openWhere} GROUP BY f.rule_id ORDER BY rank DESC, n DESC`);
|
|
5041
5056
|
return { total, sessions_flagged, dismissed, muted, by_severity, by_category, by_rule, categories };
|
|
5042
5057
|
}
|
|
5043
5058
|
|
|
@@ -5071,7 +5086,11 @@ function renderSessionExport(db, id, level) {
|
|
|
5071
5086
|
}));
|
|
5072
5087
|
const { markdown } = exportMarkdown({ id: s.id, title: s.title, source: s.source_id, project: s.project_path, model: null, started_at: s.started_at, ended_at: s.ended_at }, events, { level });
|
|
5073
5088
|
const suffix = level === "off" ? "" : ".redacted";
|
|
5074
|
-
return { markdown, filename: `session-${id.slice(0, 8)}${suffix}.md` };
|
|
5089
|
+
return { markdown, filename: `session-${filenameSafe(id.slice(0, 8))}${suffix}.md` };
|
|
5090
|
+
}
|
|
5091
|
+
function filenameSafe(s) {
|
|
5092
|
+
const cleaned = s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
5093
|
+
return cleaned === "" ? "session" : cleaned;
|
|
5075
5094
|
}
|
|
5076
5095
|
|
|
5077
5096
|
// ../server/dist/dashboard.js
|
|
@@ -5090,6 +5109,12 @@ function addUsage(acc, r) {
|
|
|
5090
5109
|
acc.cache_creation += r.cw ?? 0;
|
|
5091
5110
|
acc.cache_read += r.cr ?? 0;
|
|
5092
5111
|
}
|
|
5112
|
+
var usageForCost = (u) => ({
|
|
5113
|
+
input_tokens: u.i ?? 0,
|
|
5114
|
+
output_tokens: u.o ?? 0,
|
|
5115
|
+
cache_creation_input_tokens: u.cw ?? 0,
|
|
5116
|
+
cache_read_input_tokens: u.cr ?? 0
|
|
5117
|
+
});
|
|
5093
5118
|
function pct(sorted, p) {
|
|
5094
5119
|
if (sorted.length === 0)
|
|
5095
5120
|
return 0;
|
|
@@ -5109,11 +5134,11 @@ function workflowAgg(db, f) {
|
|
|
5109
5134
|
if (!tableExists(db, "workflow_results"))
|
|
5110
5135
|
return empty;
|
|
5111
5136
|
const ww = workflowWhere(f);
|
|
5112
|
-
const rows = db
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5137
|
+
const rows = queryAll(db, `SELECT COALESCE(status, '(unknown)') status, COUNT(*) n,
|
|
5138
|
+
SUM(COALESCE(total_tokens, 0)) tokens,
|
|
5139
|
+
SUM(COALESCE(duration_ms, 0)) dur,
|
|
5140
|
+
SUM(CASE WHEN duration_ms IS NOT NULL THEN 1 ELSE 0 END) dur_n
|
|
5141
|
+
FROM workflow_results ${ww.sql} GROUP BY status ORDER BY n DESC`, ...ww.params);
|
|
5117
5142
|
let total = 0, completed = 0, failed = 0, tokens = 0, durSum = 0, durN = 0;
|
|
5118
5143
|
const by_status = rows.map((r) => {
|
|
5119
5144
|
total += r.n;
|
|
@@ -5139,32 +5164,32 @@ function workflowAgg(db, f) {
|
|
|
5139
5164
|
}
|
|
5140
5165
|
function dashboardOverview(db, f) {
|
|
5141
5166
|
const w = sessionWhere(f);
|
|
5142
|
-
const counts = db
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
const toolCount = db
|
|
5150
|
-
const usage = db
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5167
|
+
const counts = queryGet(db, `SELECT
|
|
5168
|
+
COUNT(*) sessions,
|
|
5169
|
+
SUM(CASE WHEN is_sidechain=0 THEN 1 ELSE 0 END) main,
|
|
5170
|
+
SUM(CASE WHEN is_sidechain=1 THEN 1 ELSE 0 END) subagent,
|
|
5171
|
+
SUM(turn_count) turns,
|
|
5172
|
+
COUNT(DISTINCT project_id) projects
|
|
5173
|
+
FROM sessions s ${w.sql}`, ...w.params);
|
|
5174
|
+
const toolCount = queryGet(db, `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id ${w.sql}`, ...w.params).n;
|
|
5175
|
+
const usage = queryAll(db, `SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5176
|
+
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5177
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
|
|
5178
|
+
GROUP BY t.model`, ...w.params);
|
|
5154
5179
|
const tokens = zeroSplit();
|
|
5155
5180
|
let cost = 0;
|
|
5156
5181
|
const unpriced = [];
|
|
5157
5182
|
for (const u of usage) {
|
|
5158
5183
|
addUsage(tokens, u);
|
|
5159
|
-
cost += costForUsage(u.model,
|
|
5184
|
+
cost += costForUsage(u.model, usageForCost(u));
|
|
5160
5185
|
if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr))
|
|
5161
5186
|
unpriced.push(u.model);
|
|
5162
5187
|
}
|
|
5163
5188
|
const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
|
|
5164
|
-
const durs = db
|
|
5165
|
-
|
|
5166
|
-
const sessDurs = db
|
|
5167
|
-
|
|
5189
|
+
const durs = queryAll(db, `SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
|
|
5190
|
+
${w.sql ? w.sql + " AND" : "WHERE"} tn.duration_ms IS NOT NULL ORDER BY tn.duration_ms`, ...w.params).map((r) => r.d);
|
|
5191
|
+
const sessDurs = queryAll(db, `SELECT s.duration_ms d FROM sessions s
|
|
5192
|
+
${w.sql ? w.sql + " AND" : "WHERE"} s.is_sidechain = 0 AND s.duration_ms IS NOT NULL ORDER BY s.duration_ms`, ...w.params).map((r) => r.d);
|
|
5168
5193
|
return {
|
|
5169
5194
|
range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null },
|
|
5170
5195
|
sessions: counts.sessions ?? 0,
|
|
@@ -5186,7 +5211,7 @@ function dashboardOverview(db, f) {
|
|
|
5186
5211
|
}
|
|
5187
5212
|
function chooseBucket(db, f) {
|
|
5188
5213
|
const w = sessionWhere(f);
|
|
5189
|
-
const r = db
|
|
5214
|
+
const r = queryGet(db, `SELECT MIN(started_at) mn, MAX(started_at) mx FROM sessions s ${w.sql}`, ...w.params);
|
|
5190
5215
|
if (!r?.mn || !r?.mx)
|
|
5191
5216
|
return "day";
|
|
5192
5217
|
const spanDays = (Date.parse(r.mx) - Date.parse(r.mn)) / 864e5;
|
|
@@ -5214,26 +5239,26 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5214
5239
|
}
|
|
5215
5240
|
return r;
|
|
5216
5241
|
};
|
|
5217
|
-
for (const u of db
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5242
|
+
for (const u of queryAll(db, `SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5243
|
+
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5244
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id
|
|
5245
|
+
${w.sql} GROUP BY b, t.model`, ...w.params)) {
|
|
5221
5246
|
if (!u.b)
|
|
5222
5247
|
continue;
|
|
5223
5248
|
const r = get(u.b);
|
|
5224
5249
|
addUsage(r, u);
|
|
5225
|
-
r.cost += costForUsage(u.model,
|
|
5250
|
+
r.cost += costForUsage(u.model, usageForCost(u));
|
|
5226
5251
|
}
|
|
5227
|
-
for (const s of db
|
|
5252
|
+
for (const s of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM sessions s ${w.sql} GROUP BY b`, ...w.params)) {
|
|
5228
5253
|
if (s.b)
|
|
5229
5254
|
get(s.b).sessions = s.n;
|
|
5230
5255
|
}
|
|
5231
|
-
for (const t of db
|
|
5256
|
+
for (const t of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${w.sql} GROUP BY b`, ...w.params)) {
|
|
5232
5257
|
if (t.b)
|
|
5233
5258
|
get(t.b).turns = t.n;
|
|
5234
5259
|
}
|
|
5235
|
-
for (const e of db
|
|
5236
|
-
|
|
5260
|
+
for (const e of queryAll(db, `SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5261
|
+
${w.sql ? w.sql + " AND" : "WHERE"} tc.status = 'error' GROUP BY b, tc.error_type`, ...w.params)) {
|
|
5237
5262
|
if (!e.b)
|
|
5238
5263
|
continue;
|
|
5239
5264
|
const r = get(e.b);
|
|
@@ -5249,53 +5274,60 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5249
5274
|
}
|
|
5250
5275
|
var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
|
|
5251
5276
|
function modelBreakdown(db, w) {
|
|
5252
|
-
const modelRows =
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5277
|
+
const modelRows = queryAll(
|
|
5278
|
+
db,
|
|
5279
|
+
// COALESCE the bucket label, exactly as by_source does for a null source_id: token_usage.model is
|
|
5280
|
+
// nullable and this GROUP BY — unlike the model FILTER list — does not exclude nulls, so a
|
|
5281
|
+
// null-model bucket reaches the chart. It used to render as a nameless bar.
|
|
5282
|
+
`SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5283
|
+
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
|
|
5284
|
+
COUNT(DISTINCT t.session_id) sessions
|
|
5285
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${w.sql}
|
|
5286
|
+
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`,
|
|
5287
|
+
...w.params
|
|
5288
|
+
);
|
|
5257
5289
|
return modelRows.map((u) => ({
|
|
5258
5290
|
model: u.model,
|
|
5259
5291
|
tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
|
|
5260
5292
|
total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
|
|
5261
|
-
cost: Number(costForUsage(u.model,
|
|
5293
|
+
cost: Number(costForUsage(u.model, usageForCost(u)).toFixed(4)),
|
|
5262
5294
|
sessions: u.sessions ?? 0,
|
|
5263
5295
|
priced: !!rateForModel(u.model)
|
|
5264
5296
|
}));
|
|
5265
5297
|
}
|
|
5266
5298
|
function sourceBreakdown(db, w) {
|
|
5267
|
-
return db
|
|
5268
|
-
|
|
5299
|
+
return queryAll(db, `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, SUM(s.turn_count) turns
|
|
5300
|
+
FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`, ...w.params);
|
|
5269
5301
|
}
|
|
5270
5302
|
function classificationBreakdowns(db, w) {
|
|
5271
5303
|
const mainW = { sql: w.sql ? w.sql + " AND s.is_sidechain = 0" : "WHERE s.is_sidechain = 0", params: w.params };
|
|
5272
|
-
const byCategory = db
|
|
5273
|
-
|
|
5274
|
-
const byComplexity = db
|
|
5275
|
-
|
|
5304
|
+
const byCategory = queryAll(db, `SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
|
|
5305
|
+
${mainW.sql} GROUP BY c.category ORDER BY n DESC`, ...mainW.params);
|
|
5306
|
+
const byComplexity = queryAll(db, `SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
|
|
5307
|
+
${mainW.sql} GROUP BY c.complexity_band ORDER BY n DESC`, ...mainW.params);
|
|
5276
5308
|
return { byCategory, byComplexity };
|
|
5277
5309
|
}
|
|
5278
5310
|
function toolBreakdown(db, w) {
|
|
5279
|
-
return db
|
|
5280
|
-
|
|
5311
|
+
return queryAll(db, `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5312
|
+
${w.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`, ...w.params);
|
|
5281
5313
|
}
|
|
5282
5314
|
function skillBreakdowns(db, w) {
|
|
5283
|
-
const skills = db
|
|
5284
|
-
|
|
5285
|
-
const skillVersions = db
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5315
|
+
const skills = queryAll(db, `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5316
|
+
${andWhere(w)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`, ...w.params);
|
|
5317
|
+
const skillVersions = queryAll(db, `SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
|
|
5318
|
+
FROM tool_calls tc
|
|
5319
|
+
JOIN skills sk ON sk.id = tc.skill_id
|
|
5320
|
+
JOIN sessions s ON s.id = tc.session_id
|
|
5321
|
+
${andWhere(w)} tc.skill_id IS NOT NULL
|
|
5322
|
+
GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...w.params);
|
|
5291
5323
|
return { skills, skillVersions };
|
|
5292
5324
|
}
|
|
5293
5325
|
function subagentFanoutBreakdown(db, w) {
|
|
5294
|
-
const subagentByType = db
|
|
5295
|
-
|
|
5296
|
-
const perSession = db
|
|
5297
|
-
|
|
5298
|
-
|
|
5326
|
+
const subagentByType = queryAll(db, `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5327
|
+
${andWhere(w)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`, ...w.params);
|
|
5328
|
+
const perSession = queryAll(db, `SELECT COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5329
|
+
${andWhere(w)} s.is_sidechain = 0 AND tc.tool_name IN ('Agent','Task')
|
|
5330
|
+
GROUP BY tc.session_id`, ...w.params);
|
|
5299
5331
|
const counts = perSession.map((r) => r.n);
|
|
5300
5332
|
return {
|
|
5301
5333
|
by_type: subagentByType,
|
|
@@ -5306,8 +5338,8 @@ function subagentFanoutBreakdown(db, w) {
|
|
|
5306
5338
|
};
|
|
5307
5339
|
}
|
|
5308
5340
|
function errorTypeBreakdown(db, w) {
|
|
5309
|
-
const errorRows = db
|
|
5310
|
-
|
|
5341
|
+
const errorRows = queryAll(db, `SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc JOIN sessions s ON s.id = tc.session_id
|
|
5342
|
+
${andWhere(w)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`, ...w.params);
|
|
5311
5343
|
let failures = 0;
|
|
5312
5344
|
let rejections = 0;
|
|
5313
5345
|
const byType = errorRows.map((r) => {
|
|
@@ -5568,8 +5600,8 @@ async function createApp(db, opts = {}) {
|
|
|
5568
5600
|
errorType: q.error_type ? q.error_type.split(",").filter(Boolean) : void 0,
|
|
5569
5601
|
sort: ["started", "title", "turns", "tokens", "cost", "duration", "errors", "security"].find((s) => s === q.sort),
|
|
5570
5602
|
dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
|
|
5571
|
-
limit:
|
|
5572
|
-
offset:
|
|
5603
|
+
limit: pageLimit(q.limit, 50, 200),
|
|
5604
|
+
offset: pageOffset(q.offset)
|
|
5573
5605
|
});
|
|
5574
5606
|
});
|
|
5575
5607
|
app.get("/api/sessions/:id", async (req, reply) => {
|
|
@@ -5604,8 +5636,8 @@ async function createApp(db, opts = {}) {
|
|
|
5604
5636
|
...findingFilters(q),
|
|
5605
5637
|
sort: ["severity", "session", "rule", "category", "time"].find((s) => s === q.sort),
|
|
5606
5638
|
dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
|
|
5607
|
-
limit:
|
|
5608
|
-
offset:
|
|
5639
|
+
limit: pageLimit(q.limit, 50, 5e3),
|
|
5640
|
+
offset: pageOffset(q.offset)
|
|
5609
5641
|
});
|
|
5610
5642
|
});
|
|
5611
5643
|
app.get("/api/security/mutes", async () => triageDb ? listMutes(triageDb) : []);
|
|
@@ -5678,8 +5710,8 @@ async function createApp(db, opts = {}) {
|
|
|
5678
5710
|
project: q.project,
|
|
5679
5711
|
sort,
|
|
5680
5712
|
dir: q.dir === "asc" ? "asc" : "desc",
|
|
5681
|
-
limit:
|
|
5682
|
-
offset:
|
|
5713
|
+
limit: pageLimit(q.limit, 50, 200),
|
|
5714
|
+
offset: pageOffset(q.offset)
|
|
5683
5715
|
});
|
|
5684
5716
|
});
|
|
5685
5717
|
app.get("/api/file", async (req, reply) => {
|