@swestash/agent-lens 0.14.1 → 0.16.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/dist/agent-lens.js +446 -107
- package/dist/agent-lens.js.map +1 -1
- package/package.json +2 -2
- package/web/404.html +2 -2
- package/web/assets/{AboutView-D0_Xm4fa.js → AboutView-BExwxt7F.js} +1 -1
- package/web/assets/Dashboard-DF8Xb9E1.js +69 -0
- package/web/assets/{FileView-CnV33MsU.js → FileView-hDF8bAaa.js} +1 -1
- package/web/assets/{FilesView-DrgUr_wc.js → FilesView-DW_bHt2e.js} +1 -1
- package/web/assets/{SecurityView-1SY1h-hB.js → SecurityView-DPRI6HpG.js} +1 -1
- package/web/assets/SessionView-eGmw43Bq.js +15 -0
- package/web/assets/{SkillView-CIPe79vU.js → SkillView-BdZwKqK6.js} +1 -1
- package/web/assets/SkillsView-DUrXsvh_.js +1 -0
- package/web/assets/{WorkflowView-BXLb1lp4.js → WorkflowView-yNhKNcg9.js} +1 -1
- package/web/assets/index-B6dWnwqZ.css +1 -0
- package/web/assets/{index-Xe7pj757.js → index-BuG6Awf5.js} +1 -1
- package/web/assets/index-CMT9aRT7.js +69 -0
- package/web/assets/{jsonish-CLYQ4tku.js → jsonish-DI8SpR-J.js} +1 -1
- package/web/assets/theme-DiOdnv26.js +1 -0
- package/web/assets/{useResetOn-CpMZQynn.js → useResetOn-BEUZVwM5.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/Dashboard-ttxHcQ4v.js +0 -69
- package/web/assets/SessionView-DxxaR5_n.js +0 -15
- package/web/assets/SkillsView-YuBaj_4P.js +0 -1
- package/web/assets/index-BNr_gy9h.css +0 -1
- package/web/assets/index-CNg3TICd.js +0 -69
package/dist/agent-lens.js
CHANGED
|
@@ -812,9 +812,9 @@ CREATE INDEX IF NOT EXISTS idx_findings_category ON findings(category);
|
|
|
812
812
|
CREATE INDEX IF NOT EXISTS idx_findings_rule ON findings(rule_id);
|
|
813
813
|
CREATE INDEX IF NOT EXISTS idx_findings_tool_call ON findings(tool_call_id);
|
|
814
814
|
|
|
815
|
-
-- File modifications derived from tool calls (ADR-022). One row per (tool_call, file):
|
|
816
|
-
-- Edit/Write/NotebookEdit are 1:1
|
|
817
|
-
--
|
|
815
|
+
-- File modifications derived from tool calls (ADR-022). One row per (tool_call, file):
|
|
816
|
+
-- Edit/Write/NotebookEdit are 1:1; a Bash call can write several files, which the hash id already
|
|
817
|
+
-- accommodated without a schema break. Failed calls (status='error') are excluded
|
|
818
818
|
-- at derivation \u2014 they didn't change the file. Re-runnable like findings: incremental runs DELETE
|
|
819
819
|
-- the dirty sessions' rows and re-INSERT (delete-then-insert). Populated by ingest's
|
|
820
820
|
-- deriveFileChanges (filechanges.ts).
|
|
@@ -826,7 +826,7 @@ CREATE TABLE IF NOT EXISTS file_changes (
|
|
|
826
826
|
event_uuid TEXT, -- jump-to-transcript anchor (#ev-<uuid>)
|
|
827
827
|
project_id TEXT REFERENCES projects(id), -- from the session, for per-project grouping
|
|
828
828
|
file_path TEXT NOT NULL, -- normalized absolute path
|
|
829
|
-
tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit
|
|
829
|
+
tool_name TEXT NOT NULL, -- Edit | Write | NotebookEdit | Bash
|
|
830
830
|
lines_added INTEGER, -- newline-count delta (NULL when unknowable)
|
|
831
831
|
lines_removed INTEGER,
|
|
832
832
|
timestamp TEXT, -- ISO8601, from the tool call's event
|
|
@@ -1363,6 +1363,10 @@ function rateForModel(model) {
|
|
|
1363
1363
|
}
|
|
1364
1364
|
return best?.rate ?? null;
|
|
1365
1365
|
}
|
|
1366
|
+
var DATED_SNAPSHOT = /-\d{8}$/;
|
|
1367
|
+
function canonicalModel(model) {
|
|
1368
|
+
return model && DATED_SNAPSHOT.test(model) ? model.slice(0, -9) : model;
|
|
1369
|
+
}
|
|
1366
1370
|
function costForUsage(model, u) {
|
|
1367
1371
|
const r = rateForModel(model);
|
|
1368
1372
|
if (!r)
|
|
@@ -3423,8 +3427,98 @@ function classifyErrors(db, dirty) {
|
|
|
3423
3427
|
// ../ingest/dist/filechanges.js
|
|
3424
3428
|
import { createHash as createHash3 } from "node:crypto";
|
|
3425
3429
|
import { posix as path2 } from "node:path";
|
|
3426
|
-
|
|
3427
|
-
|
|
3430
|
+
|
|
3431
|
+
// ../ingest/dist/shellwrites.js
|
|
3432
|
+
var TARGET = `"([^"]+)"|'([^']+)'|([^\\s;|&<>()]+)`;
|
|
3433
|
+
var REDIRECT = new RegExp(`(?:^|[\\s;|&(])\\d?>{1,2}\\s*(?:${TARGET})`, "g");
|
|
3434
|
+
var TEE = new RegExp(`(?:^|[\\s;|&(])tee\\s+(?:-a\\s+)?(?:${TARGET})`, "g");
|
|
3435
|
+
var SED_I = new RegExp(`(?:^|[\\s;|&(])sed\\s+(?:-[a-zA-Z]*i[a-zA-Z]*(?:\\.\\w+)?)\\s+(?:(?:-e\\s+)?(?:'[^']*'|"[^"]*"|\\S+)\\s+)+?(?:${TARGET})`, "g");
|
|
3436
|
+
var CD = new RegExp(`^\\s*cd\\s+(?:${TARGET})`);
|
|
3437
|
+
var HEREDOC = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1/g;
|
|
3438
|
+
var REDIRECT_BEFORE_HEREDOC = new RegExp(`\\d?>{1,2}\\s*(?:${TARGET})\\s*$`);
|
|
3439
|
+
function target(m, first) {
|
|
3440
|
+
return m[first] ?? m[first + 1] ?? m[first + 2] ?? null;
|
|
3441
|
+
}
|
|
3442
|
+
function usable(path3) {
|
|
3443
|
+
if (!path3)
|
|
3444
|
+
return false;
|
|
3445
|
+
if (/[$*?`~]/.test(path3))
|
|
3446
|
+
return false;
|
|
3447
|
+
if (path3.startsWith("&"))
|
|
3448
|
+
return false;
|
|
3449
|
+
if (path3 === "/dev/null" || path3.startsWith("/dev/"))
|
|
3450
|
+
return false;
|
|
3451
|
+
return true;
|
|
3452
|
+
}
|
|
3453
|
+
function bodyLines(body) {
|
|
3454
|
+
const trimmed = body.replace(/\n$/, "");
|
|
3455
|
+
return trimmed === "" ? 0 : trimmed.split("\n").length;
|
|
3456
|
+
}
|
|
3457
|
+
function stripHeredocs(command) {
|
|
3458
|
+
const writes = [];
|
|
3459
|
+
let scan = command;
|
|
3460
|
+
HEREDOC.lastIndex = 0;
|
|
3461
|
+
let m;
|
|
3462
|
+
while (m = HEREDOC.exec(scan)) {
|
|
3463
|
+
const delim = m[2];
|
|
3464
|
+
const openerEnd = scan.indexOf("\n", m.index);
|
|
3465
|
+
if (openerEnd === -1)
|
|
3466
|
+
break;
|
|
3467
|
+
const terminator = new RegExp(`^[ \\t]*${delim}[ \\t]*$`, "m");
|
|
3468
|
+
const rest = scan.slice(openerEnd + 1);
|
|
3469
|
+
const end = rest.search(terminator);
|
|
3470
|
+
const bodyEnd = end === -1 ? rest.length : end;
|
|
3471
|
+
const before = scan.slice(0, m.index);
|
|
3472
|
+
const red = before.match(REDIRECT_BEFORE_HEREDOC);
|
|
3473
|
+
if (red) {
|
|
3474
|
+
const p = target(red, 1);
|
|
3475
|
+
if (usable(p))
|
|
3476
|
+
writes.push({ at: red.index ?? m.index, write: { path: p, linesAdded: bodyLines(rest.slice(0, bodyEnd)) } });
|
|
3477
|
+
}
|
|
3478
|
+
const stripEnd = end === -1 ? scan.length : openerEnd + 1 + bodyEnd + delim.length;
|
|
3479
|
+
scan = scan.slice(0, m.index) + " ".repeat(stripEnd - m.index) + scan.slice(stripEnd);
|
|
3480
|
+
HEREDOC.lastIndex = stripEnd;
|
|
3481
|
+
}
|
|
3482
|
+
return { scan, writes };
|
|
3483
|
+
}
|
|
3484
|
+
function parseShellWrites(command) {
|
|
3485
|
+
if (typeof command !== "string" || !command)
|
|
3486
|
+
return { cwd: null, writes: [] };
|
|
3487
|
+
const cdMatch = command.match(CD);
|
|
3488
|
+
const cdPath = cdMatch ? target(cdMatch, 1) : null;
|
|
3489
|
+
const cwd = cdPath && cdPath.startsWith("/") && !/[$*?`~]/.test(cdPath) ? cdPath : null;
|
|
3490
|
+
const { scan, writes: found } = stripHeredocs(command);
|
|
3491
|
+
for (const [re, group] of [
|
|
3492
|
+
[REDIRECT, 1],
|
|
3493
|
+
[TEE, 1],
|
|
3494
|
+
[SED_I, 1]
|
|
3495
|
+
]) {
|
|
3496
|
+
re.lastIndex = 0;
|
|
3497
|
+
let m;
|
|
3498
|
+
while (m = re.exec(scan)) {
|
|
3499
|
+
const p = target(m, group);
|
|
3500
|
+
if (usable(p))
|
|
3501
|
+
found.push({ at: m.index, write: { path: p, linesAdded: null } });
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
3505
|
+
for (const entry of found) {
|
|
3506
|
+
const prev = byPath.get(entry.write.path);
|
|
3507
|
+
if (!prev) {
|
|
3508
|
+
byPath.set(entry.write.path, entry);
|
|
3509
|
+
continue;
|
|
3510
|
+
}
|
|
3511
|
+
byPath.set(entry.write.path, {
|
|
3512
|
+
at: Math.min(prev.at, entry.at),
|
|
3513
|
+
write: { path: entry.write.path, linesAdded: prev.write.linesAdded ?? entry.write.linesAdded }
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3516
|
+
return { cwd, writes: [...byPath.values()].sort((a, b) => a.at - b.at).map((e) => e.write) };
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
// ../ingest/dist/filechanges.js
|
|
3520
|
+
var FILECHANGES_VERSION = 2;
|
|
3521
|
+
var FILE_TOOLS = ["Edit", "Write", "NotebookEdit", "Bash"];
|
|
3428
3522
|
function fileChangeId(toolCallId, filePath) {
|
|
3429
3523
|
return createHash3("sha1").update(toolCallId).update("\0").update(filePath).digest("hex").slice(0, 16);
|
|
3430
3524
|
}
|
|
@@ -3485,31 +3579,40 @@ function deriveFileChanges(db, dirty) {
|
|
|
3485
3579
|
if (!input)
|
|
3486
3580
|
continue;
|
|
3487
3581
|
const p = proj.get(row.session_id);
|
|
3488
|
-
const
|
|
3489
|
-
if (
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
} else
|
|
3497
|
-
|
|
3582
|
+
const targets = [];
|
|
3583
|
+
if (row.tool_name === "Bash") {
|
|
3584
|
+
const { cwd, writes } = parseShellWrites(input.command);
|
|
3585
|
+
for (const w of writes) {
|
|
3586
|
+
const filePath = normalizeFilePath(w.path, cwd ?? p?.path ?? null);
|
|
3587
|
+
if (filePath)
|
|
3588
|
+
targets.push({ filePath, added: w.linesAdded, removed: null });
|
|
3589
|
+
}
|
|
3590
|
+
} else {
|
|
3591
|
+
const filePath = normalizeFilePath(input.file_path ?? input.notebook_path, p?.path ?? null);
|
|
3592
|
+
if (filePath) {
|
|
3593
|
+
targets.push({
|
|
3594
|
+
filePath,
|
|
3595
|
+
added: row.tool_name === "Edit" ? lineCount(input.new_string) : row.tool_name === "Write" ? lineCount(input.content) : null,
|
|
3596
|
+
removed: row.tool_name === "Edit" ? lineCount(input.old_string) : null
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
for (const t of targets) {
|
|
3601
|
+
insert.run({
|
|
3602
|
+
id: fileChangeId(row.id, t.filePath),
|
|
3603
|
+
tool_call_id: row.id,
|
|
3604
|
+
session_id: row.session_id,
|
|
3605
|
+
turn_id: row.turn_id,
|
|
3606
|
+
event_uuid: row.event_uuid,
|
|
3607
|
+
project_id: p?.id ?? null,
|
|
3608
|
+
file_path: t.filePath,
|
|
3609
|
+
tool_name: row.tool_name,
|
|
3610
|
+
lines_added: t.added,
|
|
3611
|
+
lines_removed: t.removed,
|
|
3612
|
+
timestamp: row.timestamp,
|
|
3613
|
+
derive_version: FILECHANGES_VERSION
|
|
3614
|
+
});
|
|
3498
3615
|
}
|
|
3499
|
-
insert.run({
|
|
3500
|
-
id: fileChangeId(row.id, filePath),
|
|
3501
|
-
tool_call_id: row.id,
|
|
3502
|
-
session_id: row.session_id,
|
|
3503
|
-
turn_id: row.turn_id,
|
|
3504
|
-
event_uuid: row.event_uuid,
|
|
3505
|
-
project_id: p?.id ?? null,
|
|
3506
|
-
file_path: filePath,
|
|
3507
|
-
tool_name: row.tool_name,
|
|
3508
|
-
lines_added: added,
|
|
3509
|
-
lines_removed: removed,
|
|
3510
|
-
timestamp: row.timestamp,
|
|
3511
|
-
derive_version: FILECHANGES_VERSION
|
|
3512
|
-
});
|
|
3513
3616
|
}
|
|
3514
3617
|
});
|
|
3515
3618
|
tx();
|
|
@@ -3581,15 +3684,15 @@ function canonicalizeProjects(db, opts = {}) {
|
|
|
3581
3684
|
const touched = /* @__PURE__ */ new Set();
|
|
3582
3685
|
transaction(db, () => {
|
|
3583
3686
|
for (const r of rows) {
|
|
3584
|
-
const
|
|
3585
|
-
if (
|
|
3687
|
+
const target2 = canon(r.path);
|
|
3688
|
+
if (target2 === strip(r.path))
|
|
3586
3689
|
continue;
|
|
3587
|
-
const targetId = byPath.get(
|
|
3690
|
+
const targetId = byPath.get(target2)?.id ?? projectId(r.agent_id, target2);
|
|
3588
3691
|
if (targetId === r.id)
|
|
3589
3692
|
continue;
|
|
3590
|
-
if (!byPath.has(
|
|
3591
|
-
upsertProject.run(targetId, r.agent_id,
|
|
3592
|
-
byPath.set(
|
|
3693
|
+
if (!byPath.has(target2)) {
|
|
3694
|
+
upsertProject.run(targetId, r.agent_id, target2);
|
|
3695
|
+
byPath.set(target2, { id: targetId, agent_id: r.agent_id });
|
|
3593
3696
|
}
|
|
3594
3697
|
repointSessions.run(targetId, r.id);
|
|
3595
3698
|
repointFileChanges.run(targetId, r.id);
|
|
@@ -4568,6 +4671,10 @@ function pushDateRange(clauses, params, col, from, to) {
|
|
|
4568
4671
|
params.push(to);
|
|
4569
4672
|
}
|
|
4570
4673
|
}
|
|
4674
|
+
function canonModelSql(col) {
|
|
4675
|
+
return `CASE WHEN ${col} GLOB '*-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
|
|
4676
|
+
THEN substr(${col}, 1, length(${col}) - 9) ELSE ${col} END`;
|
|
4677
|
+
}
|
|
4571
4678
|
function metaJoin(hasMeta) {
|
|
4572
4679
|
return hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : "";
|
|
4573
4680
|
}
|
|
@@ -4657,7 +4764,8 @@ function listProjects(db) {
|
|
|
4657
4764
|
FROM projects p ORDER BY session_count DESC, p.path`);
|
|
4658
4765
|
}
|
|
4659
4766
|
function listModels(db) {
|
|
4660
|
-
return queryAll(db, `SELECT DISTINCT
|
|
4767
|
+
return queryAll(db, `SELECT DISTINCT ${canonModelSql("m")} AS model
|
|
4768
|
+
FROM (SELECT DISTINCT model m FROM token_usage WHERE model IS NOT NULL) ORDER BY 1`).map((r) => r.model);
|
|
4661
4769
|
}
|
|
4662
4770
|
function toFtsQuery(raw) {
|
|
4663
4771
|
return raw.trim().split(/\s+/).filter(Boolean).map((t) => `"${t.replace(/"/g, '""')}"`).join(" ");
|
|
@@ -4709,8 +4817,8 @@ function listSessions(db, f) {
|
|
|
4709
4817
|
params.push(...f.errorType);
|
|
4710
4818
|
}
|
|
4711
4819
|
if (f.model) {
|
|
4712
|
-
where.push(
|
|
4713
|
-
params.push(f.model);
|
|
4820
|
+
where.push(`EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND ${canonModelSql("t.model")} = ?)`);
|
|
4821
|
+
params.push(canonicalModel(f.model));
|
|
4714
4822
|
}
|
|
4715
4823
|
if (f.q && f.q.trim()) {
|
|
4716
4824
|
const like = `%${f.q.trim()}%`;
|
|
@@ -4723,7 +4831,7 @@ function listSessions(db, f) {
|
|
|
4723
4831
|
const total = queryGet(db, `SELECT COUNT(*) n FROM sessions s ${whereSql}`, ...params).n;
|
|
4724
4832
|
const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at,
|
|
4725
4833
|
s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
|
|
4726
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models,
|
|
4834
|
+
(SELECT GROUP_CONCAT(DISTINCT ${canonModelSql("model")}) FROM token_usage t WHERE t.session_id = s.id) AS models,
|
|
4727
4835
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id) AS tool_call_count,
|
|
4728
4836
|
(SELECT COUNT(*) FROM tool_calls tc WHERE tc.session_id = s.id AND tc.status = 'error') AS tool_error_count,
|
|
4729
4837
|
(SELECT COUNT(*) FROM findings fd WHERE fd.session_id = s.id) AS finding_count,
|
|
@@ -4827,8 +4935,51 @@ function loadFindings(db, id, toolRows) {
|
|
|
4827
4935
|
function toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }) {
|
|
4828
4936
|
return call;
|
|
4829
4937
|
}
|
|
4938
|
+
function rendersSomething(e, toolsByEvent) {
|
|
4939
|
+
return !!(e.text || e.thinking || toolsByEvent.get(e.uuid)?.length);
|
|
4940
|
+
}
|
|
4941
|
+
function addSplit(a, b) {
|
|
4942
|
+
if (!b)
|
|
4943
|
+
return a;
|
|
4944
|
+
if (!a)
|
|
4945
|
+
return { ...b };
|
|
4946
|
+
return {
|
|
4947
|
+
input: a.input + b.input,
|
|
4948
|
+
output: a.output + b.output,
|
|
4949
|
+
cache_creation: a.cache_creation + b.cache_creation,
|
|
4950
|
+
cache_read: a.cache_read + b.cache_read
|
|
4951
|
+
};
|
|
4952
|
+
}
|
|
4953
|
+
function foldUsage(eventRows, toolsByEvent, usageByEvent) {
|
|
4954
|
+
const out = /* @__PURE__ */ new Map();
|
|
4955
|
+
if (usageByEvent.size === 0)
|
|
4956
|
+
return out;
|
|
4957
|
+
const byTurn = /* @__PURE__ */ new Map();
|
|
4958
|
+
for (const e of eventRows)
|
|
4959
|
+
pushGrouped(byTurn, e.turn_id ?? "", e);
|
|
4960
|
+
for (const group of byTurn.values()) {
|
|
4961
|
+
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
4962
|
+
let pending = null;
|
|
4963
|
+
let lastRendering = null;
|
|
4964
|
+
for (const e of ordered) {
|
|
4965
|
+
const own = usageByEvent.get(e.uuid);
|
|
4966
|
+
if (rendersSomething(e, toolsByEvent)) {
|
|
4967
|
+
const total = addSplit(pending, own);
|
|
4968
|
+
if (total)
|
|
4969
|
+
out.set(e.uuid, total);
|
|
4970
|
+
pending = null;
|
|
4971
|
+
lastRendering = e.uuid;
|
|
4972
|
+
} else {
|
|
4973
|
+
pending = addSplit(pending, own);
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
if (pending && lastRendering)
|
|
4977
|
+
out.set(lastRendering, addSplit(out.get(lastRendering) ?? null, pending));
|
|
4978
|
+
}
|
|
4979
|
+
return out;
|
|
4980
|
+
}
|
|
4830
4981
|
function loadEvents(db, id, toolRows) {
|
|
4831
|
-
const eventRows = queryAll(db, `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, text, thinking
|
|
4982
|
+
const eventRows = queryAll(db, `SELECT uuid, seq, type, role, timestamp, model, is_sidechain, turn_id, text, thinking
|
|
4832
4983
|
FROM events WHERE session_id = ? ORDER BY timestamp, seq`, id);
|
|
4833
4984
|
const toolsByEvent = /* @__PURE__ */ new Map();
|
|
4834
4985
|
for (const t of toolRows) {
|
|
@@ -4836,18 +4987,30 @@ function loadEvents(db, id, toolRows) {
|
|
|
4836
4987
|
continue;
|
|
4837
4988
|
pushGrouped(toolsByEvent, t.event_uuid, t);
|
|
4838
4989
|
}
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4990
|
+
const usageByEvent = /* @__PURE__ */ new Map();
|
|
4991
|
+
for (const u of queryAll(db, `SELECT event_uuid, input_tokens i, output_tokens o, cache_creation_input_tokens cw,
|
|
4992
|
+
cache_read_input_tokens cr
|
|
4993
|
+
FROM token_usage WHERE session_id = ?`, id)) {
|
|
4994
|
+
usageByEvent.set(u.event_uuid, { input: u.i, output: u.o, cache_creation: u.cw, cache_read: u.cr });
|
|
4995
|
+
}
|
|
4996
|
+
const folded = foldUsage(eventRows, toolsByEvent, usageByEvent);
|
|
4997
|
+
return eventRows.map((e) => {
|
|
4998
|
+
const usage = folded.get(e.uuid);
|
|
4999
|
+
return {
|
|
5000
|
+
uuid: e.uuid,
|
|
5001
|
+
type: e.type,
|
|
5002
|
+
role: e.role,
|
|
5003
|
+
timestamp: e.timestamp,
|
|
5004
|
+
model: e.model,
|
|
5005
|
+
is_sidechain: e.is_sidechain,
|
|
5006
|
+
turn_id: e.turn_id,
|
|
5007
|
+
text: e.text,
|
|
5008
|
+
thinking: e.thinking,
|
|
5009
|
+
toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall),
|
|
5010
|
+
// Spread so the key is ABSENT, not null, on the majority of events that carry no usage row.
|
|
5011
|
+
...usage ? { usage } : {}
|
|
5012
|
+
};
|
|
5013
|
+
});
|
|
4851
5014
|
}
|
|
4852
5015
|
function loadClassification(db, id) {
|
|
4853
5016
|
const cls = queryGet(db, `SELECT category, complexity_score, complexity_band, signals_json, classifier_version
|
|
@@ -4878,7 +5041,7 @@ function loadParent(db, session) {
|
|
|
4878
5041
|
function loadChildren(db, id) {
|
|
4879
5042
|
const hasMeta = tableExists(db, "session_meta");
|
|
4880
5043
|
const children = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
|
|
4881
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
5044
|
+
(SELECT GROUP_CONCAT(DISTINCT ${canonModelSql("model")}) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4882
5045
|
${metaProjection(hasMeta)}
|
|
4883
5046
|
FROM sessions s ${metaJoin(hasMeta)}
|
|
4884
5047
|
WHERE s.parent_session_id = ? ORDER BY s.started_at`, id);
|
|
@@ -4954,7 +5117,7 @@ function safeJson(s) {
|
|
|
4954
5117
|
function loadAgents(db, runId) {
|
|
4955
5118
|
const hasMeta = tableExists(db, "session_meta");
|
|
4956
5119
|
const agentRows = queryAll(db, `SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
|
|
4957
|
-
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
5120
|
+
(SELECT GROUP_CONCAT(DISTINCT ${canonModelSql("model")}) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
4958
5121
|
${metaProjection(hasMeta)}
|
|
4959
5122
|
FROM sessions s ${metaJoin(hasMeta)}
|
|
4960
5123
|
WHERE s.workflow_run_id = ? ORDER BY s.started_at`, runId);
|
|
@@ -5348,8 +5511,24 @@ function sessionWhere(f) {
|
|
|
5348
5511
|
if (f.source)
|
|
5349
5512
|
where.push("s.source_id = ?"), params.push(f.source);
|
|
5350
5513
|
pushDateRange(where, params, "s.started_at", f.from, f.to);
|
|
5351
|
-
|
|
5352
|
-
|
|
5514
|
+
const models = f.models?.length ? [...new Set(f.models.map((m) => canonicalModel(m)))] : void 0;
|
|
5515
|
+
if (models) {
|
|
5516
|
+
where.push(`EXISTS (SELECT 1 FROM token_usage t WHERE t.session_id = s.id AND ${canonModelSql("t.model")} IN (${placeholders(models)}))`);
|
|
5517
|
+
params.push(...models);
|
|
5518
|
+
}
|
|
5519
|
+
return { sql: where.length ? `WHERE ${where.join(" AND ")}` : "", params, models };
|
|
5520
|
+
}
|
|
5521
|
+
var placeholders = (xs) => xs.map(() => "?").join(",");
|
|
5522
|
+
var and = (w, sql, params = []) => ({
|
|
5523
|
+
sql: `${andWhere(w)} ${sql}`,
|
|
5524
|
+
params: [...w.params, ...params],
|
|
5525
|
+
models: w.models
|
|
5526
|
+
});
|
|
5527
|
+
var withModel = (w, col) => w.models ? and(w, `${canonModelSql(col)} IN (${placeholders(w.models)})`, w.models) : w;
|
|
5528
|
+
var toolModelJoin = (w) => w.models ? "JOIN events e ON e.uuid = tc.event_uuid" : "";
|
|
5529
|
+
var eventJoin = "JOIN events e ON e.uuid = tc.event_uuid";
|
|
5530
|
+
var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
|
|
5531
|
+
var mainOnly = (w) => ({ sql: `${andWhere(w)} s.is_sidechain = 0`, params: w.params, models: w.models });
|
|
5353
5532
|
var zeroSplit = () => ({ input: 0, output: 0, cache_creation: 0, cache_read: 0 });
|
|
5354
5533
|
function addUsage(acc, r) {
|
|
5355
5534
|
acc.input += r.i ?? 0;
|
|
@@ -5426,11 +5605,13 @@ function dashboardOverview(db, f) {
|
|
|
5426
5605
|
SUM(turn_count) turns,
|
|
5427
5606
|
COUNT(DISTINCT project_id) projects
|
|
5428
5607
|
FROM sessions s ${w.sql}`, ...w.params);
|
|
5429
|
-
const
|
|
5608
|
+
const tw = withModel(w, "e.model");
|
|
5609
|
+
const toolCount = queryGet(db, `SELECT COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id ${tw.sql}`, ...tw.params).n;
|
|
5610
|
+
const uw = withModel(w, "t.model");
|
|
5430
5611
|
const usage = queryAll(db, `SELECT t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5431
5612
|
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5432
|
-
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${
|
|
5433
|
-
GROUP BY t.model`, ...
|
|
5613
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${uw.sql}
|
|
5614
|
+
GROUP BY t.model`, ...uw.params);
|
|
5434
5615
|
const tokens = zeroSplit();
|
|
5435
5616
|
let cost = 0;
|
|
5436
5617
|
const unpriced2 = [];
|
|
@@ -5441,16 +5622,20 @@ function dashboardOverview(db, f) {
|
|
|
5441
5622
|
unpriced2.push(u.model);
|
|
5442
5623
|
}
|
|
5443
5624
|
const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
|
|
5625
|
+
const tnw = withModel(w, "tn.model");
|
|
5626
|
+
const turns = tnw.models ? queryGet(db, `SELECT COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${tnw.sql}`, ...tnw.params)?.n ?? 0 : counts.turns ?? 0;
|
|
5444
5627
|
const turnDur = percentiles(db, `SELECT tn.duration_ms v FROM turns tn JOIN sessions s ON s.id = tn.session_id
|
|
5445
|
-
${
|
|
5446
|
-
const
|
|
5447
|
-
|
|
5628
|
+
${andWhere(tnw)} tn.duration_ms IS NOT NULL`, tnw.params);
|
|
5629
|
+
const mw = mainOnly(w);
|
|
5630
|
+
const sessDur = percentiles(db, `SELECT s.duration_ms v FROM sessions s ${mw.sql} AND s.duration_ms IS NOT NULL`, mw.params);
|
|
5448
5631
|
return {
|
|
5449
|
-
|
|
5632
|
+
// `w.models`, not `f.models`: the echo names the models actually queried, so a link that arrived
|
|
5633
|
+
// with a raw dated id reports the family key it resolved to rather than the string it was sent.
|
|
5634
|
+
range: { from: f.from ?? null, to: f.to ?? null, source: f.source ?? null, models: w.models ?? null },
|
|
5450
5635
|
sessions: counts.sessions ?? 0,
|
|
5451
5636
|
sessions_main: counts.main ?? 0,
|
|
5452
5637
|
sessions_subagent: counts.subagent ?? 0,
|
|
5453
|
-
turns
|
|
5638
|
+
turns,
|
|
5454
5639
|
projects: counts.projects ?? 0,
|
|
5455
5640
|
tool_calls: toolCount ?? 0,
|
|
5456
5641
|
tokens,
|
|
@@ -5476,15 +5661,11 @@ function chooseBucket(db, f) {
|
|
|
5476
5661
|
return "week";
|
|
5477
5662
|
return "month";
|
|
5478
5663
|
}
|
|
5479
|
-
var
|
|
5480
|
-
day: "strftime('%Y-%m-%d', s.started_at)",
|
|
5481
|
-
week: "strftime('%Y-W%W', s.started_at)",
|
|
5482
|
-
month: "strftime('%Y-%m', s.started_at)"
|
|
5483
|
-
};
|
|
5664
|
+
var bucketExpr = (b, col) => b === "day" ? `strftime('%Y-%m-%d', ${col})` : b === "week" ? `strftime('%Y-W%W', ${col})` : `strftime('%Y-%m', ${col})`;
|
|
5484
5665
|
function dashboardTimeseries(db, f, bucketParam) {
|
|
5485
5666
|
const bucket = bucketParam === "day" || bucketParam === "week" || bucketParam === "month" ? bucketParam : chooseBucket(db, f);
|
|
5486
5667
|
const w = sessionWhere(f);
|
|
5487
|
-
const expr =
|
|
5668
|
+
const expr = bucketExpr(bucket, "s.started_at");
|
|
5488
5669
|
const byBucket = /* @__PURE__ */ new Map();
|
|
5489
5670
|
const get = (b) => {
|
|
5490
5671
|
let r = byBucket.get(b);
|
|
@@ -5494,10 +5675,13 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5494
5675
|
}
|
|
5495
5676
|
return r;
|
|
5496
5677
|
};
|
|
5678
|
+
const uw = withModel(w, "t.model");
|
|
5679
|
+
const tnw = withModel(w, "tn.model");
|
|
5680
|
+
const ew = withModel(w, "e.model");
|
|
5497
5681
|
for (const u of queryAll(db, `SELECT ${expr} b, t.model model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5498
5682
|
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5499
5683
|
FROM token_usage t JOIN sessions s ON s.id = t.session_id
|
|
5500
|
-
${
|
|
5684
|
+
${uw.sql} GROUP BY b, t.model`, ...uw.params)) {
|
|
5501
5685
|
if (!u.b)
|
|
5502
5686
|
continue;
|
|
5503
5687
|
const r = get(u.b);
|
|
@@ -5508,12 +5692,12 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5508
5692
|
if (s.b)
|
|
5509
5693
|
get(s.b).sessions = s.n;
|
|
5510
5694
|
}
|
|
5511
|
-
for (const t of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${
|
|
5695
|
+
for (const t of queryAll(db, `SELECT ${expr} b, COUNT(*) n FROM turns tn JOIN sessions s ON s.id = tn.session_id ${tnw.sql} GROUP BY b`, ...tnw.params)) {
|
|
5512
5696
|
if (t.b)
|
|
5513
5697
|
get(t.b).turns = t.n;
|
|
5514
5698
|
}
|
|
5515
|
-
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
|
|
5516
|
-
${
|
|
5699
|
+
for (const e of queryAll(db, `SELECT ${expr} b, tc.error_type et, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5700
|
+
${andWhere(ew)} tc.status = 'error' GROUP BY b, tc.error_type`, ...ew.params)) {
|
|
5517
5701
|
if (!e.b)
|
|
5518
5702
|
continue;
|
|
5519
5703
|
const r = get(e.b);
|
|
@@ -5527,35 +5711,40 @@ function dashboardTimeseries(db, f, bucketParam) {
|
|
|
5527
5711
|
r.cost = Number(r.cost.toFixed(4));
|
|
5528
5712
|
return { bucket, series };
|
|
5529
5713
|
}
|
|
5530
|
-
var andWhere = (w) => w.sql ? w.sql + " AND" : "WHERE";
|
|
5531
5714
|
function modelBreakdown(db, w) {
|
|
5532
|
-
const
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
// nullable and this GROUP BY — unlike the model FILTER list — does not exclude nulls, so a
|
|
5536
|
-
// null-model bucket reaches the chart. It used to render as a nameless bar.
|
|
5537
|
-
`SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5715
|
+
const uw = withModel(w, "t.model");
|
|
5716
|
+
const canon = canonModelSql("t.model");
|
|
5717
|
+
const modelRows = queryAll(db, `SELECT COALESCE(${canon}, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5538
5718
|
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr,
|
|
5539
5719
|
COUNT(DISTINCT t.session_id) sessions
|
|
5540
|
-
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${
|
|
5541
|
-
GROUP BY
|
|
5542
|
-
|
|
5543
|
-
);
|
|
5720
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${uw.sql}
|
|
5721
|
+
GROUP BY 1 ORDER BY (SUM(t.input_tokens)+SUM(t.output_tokens)+SUM(t.cache_creation_input_tokens)+SUM(t.cache_read_input_tokens)) DESC`, ...uw.params);
|
|
5722
|
+
const cost = /* @__PURE__ */ new Map();
|
|
5723
|
+
const unpriced2 = /* @__PURE__ */ new Set();
|
|
5724
|
+
for (const u of queryAll(db, `SELECT COALESCE(t.model, '(unknown)') model, SUM(t.input_tokens) i, SUM(t.output_tokens) o,
|
|
5725
|
+
SUM(t.cache_creation_input_tokens) cw, SUM(t.cache_read_input_tokens) cr
|
|
5726
|
+
FROM token_usage t JOIN sessions s ON s.id = t.session_id ${uw.sql} GROUP BY t.model`, ...uw.params)) {
|
|
5727
|
+
const key = canonicalModel(u.model);
|
|
5728
|
+
cost.set(key, (cost.get(key) ?? 0) + costForUsage(u.model, usageForCost(u)));
|
|
5729
|
+
if (!rateForModel(u.model))
|
|
5730
|
+
unpriced2.add(key);
|
|
5731
|
+
}
|
|
5544
5732
|
return modelRows.map((u) => ({
|
|
5545
5733
|
model: u.model,
|
|
5546
5734
|
tokens: { input: u.i ?? 0, output: u.o ?? 0, cache_creation: u.cw ?? 0, cache_read: u.cr ?? 0 },
|
|
5547
5735
|
total_tokens: (u.i ?? 0) + (u.o ?? 0) + (u.cw ?? 0) + (u.cr ?? 0),
|
|
5548
|
-
cost: Number(
|
|
5736
|
+
cost: Number((cost.get(u.model) ?? 0).toFixed(4)),
|
|
5549
5737
|
sessions: u.sessions ?? 0,
|
|
5550
|
-
priced:
|
|
5738
|
+
priced: !unpriced2.has(u.model)
|
|
5551
5739
|
}));
|
|
5552
5740
|
}
|
|
5553
5741
|
function sourceBreakdown(db, w) {
|
|
5554
|
-
|
|
5555
|
-
|
|
5742
|
+
const perSourceTurns = w.models ? `SUM((SELECT COUNT(*) FROM turns tn WHERE tn.session_id = s.id AND ${canonModelSql("tn.model")} IN (${placeholders(w.models)})))` : "SUM(s.turn_count)";
|
|
5743
|
+
return queryAll(db, `SELECT COALESCE(s.source_id, '(none)') source, COUNT(*) sessions, ${perSourceTurns} turns
|
|
5744
|
+
FROM sessions s ${w.sql} GROUP BY s.source_id ORDER BY sessions DESC`, ...w.models ?? [], ...w.params);
|
|
5556
5745
|
}
|
|
5557
5746
|
function classificationBreakdowns(db, w) {
|
|
5558
|
-
const mainW =
|
|
5747
|
+
const mainW = mainOnly(w);
|
|
5559
5748
|
const byCategory = queryAll(db, `SELECT c.category, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
|
|
5560
5749
|
${mainW.sql} GROUP BY c.category ORDER BY n DESC`, ...mainW.params);
|
|
5561
5750
|
const byComplexity = queryAll(db, `SELECT c.complexity_band band, COUNT(*) n FROM classifications c JOIN sessions s ON s.id = c.target_id
|
|
@@ -5563,26 +5752,30 @@ function classificationBreakdowns(db, w) {
|
|
|
5563
5752
|
return { byCategory, byComplexity };
|
|
5564
5753
|
}
|
|
5565
5754
|
function toolBreakdown(db, w) {
|
|
5566
|
-
|
|
5567
|
-
|
|
5755
|
+
const ew = withModel(w, "e.model");
|
|
5756
|
+
return queryAll(db, `SELECT tc.tool_name name, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5757
|
+
${ew.sql} GROUP BY tc.tool_name ORDER BY n DESC LIMIT 20`, ...ew.params);
|
|
5568
5758
|
}
|
|
5569
5759
|
function skillBreakdowns(db, w) {
|
|
5570
|
-
const
|
|
5571
|
-
|
|
5760
|
+
const ew = withModel(w, "e.model");
|
|
5761
|
+
const skills = queryAll(db, `SELECT tc.skill_name name, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5762
|
+
${andWhere(ew)} tc.skill_name IS NOT NULL GROUP BY tc.skill_name ORDER BY n DESC LIMIT 20`, ...ew.params);
|
|
5572
5763
|
const skillVersions = queryAll(db, `SELECT tc.skill_name name, sk.id version_id, sk.summary, sk.last_seen, COUNT(*) n
|
|
5573
5764
|
FROM tool_calls tc
|
|
5765
|
+
${toolModelJoin(w)}
|
|
5574
5766
|
JOIN skills sk ON sk.id = tc.skill_id
|
|
5575
5767
|
JOIN sessions s ON s.id = tc.session_id
|
|
5576
|
-
${andWhere(
|
|
5577
|
-
GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...
|
|
5768
|
+
${andWhere(ew)} tc.skill_id IS NOT NULL
|
|
5769
|
+
GROUP BY tc.skill_name, sk.id ORDER BY name, n DESC`, ...ew.params);
|
|
5578
5770
|
return { skills, skillVersions };
|
|
5579
5771
|
}
|
|
5580
5772
|
function subagentFanoutBreakdown(db, w) {
|
|
5581
|
-
const
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5773
|
+
const ew = withModel(w, "e.model");
|
|
5774
|
+
const subagentByType = queryAll(db, `SELECT tc.agent_type type, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5775
|
+
${andWhere(ew)} tc.agent_type IS NOT NULL GROUP BY tc.agent_type ORDER BY n DESC`, ...ew.params);
|
|
5776
|
+
const perSession = queryAll(db, `SELECT COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5777
|
+
${mainOnly(ew).sql} AND tc.tool_name IN ('Agent','Task')
|
|
5778
|
+
GROUP BY tc.session_id`, ...ew.params);
|
|
5586
5779
|
const counts = perSession.map((r) => r.n);
|
|
5587
5780
|
return {
|
|
5588
5781
|
by_type: subagentByType,
|
|
@@ -5593,8 +5786,9 @@ function subagentFanoutBreakdown(db, w) {
|
|
|
5593
5786
|
};
|
|
5594
5787
|
}
|
|
5595
5788
|
function errorTypeBreakdown(db, w) {
|
|
5596
|
-
const
|
|
5597
|
-
|
|
5789
|
+
const ew = withModel(w, "e.model");
|
|
5790
|
+
const errorRows = queryAll(db, `SELECT COALESCE(tc.error_type, 'other') type, COUNT(*) n FROM tool_calls tc ${toolModelJoin(w)} JOIN sessions s ON s.id = tc.session_id
|
|
5791
|
+
${andWhere(ew)} tc.status = 'error' GROUP BY tc.error_type ORDER BY n DESC`, ...ew.params);
|
|
5598
5792
|
let failures = 0;
|
|
5599
5793
|
let rejections = 0;
|
|
5600
5794
|
const byType = errorRows.map((r) => {
|
|
@@ -5623,6 +5817,143 @@ function dashboardBreakdowns(db, f) {
|
|
|
5623
5817
|
error_types: errorTypeBreakdown(db, w)
|
|
5624
5818
|
};
|
|
5625
5819
|
}
|
|
5820
|
+
var bucketFor = (b) => b === "day" || b === "week" || b === "month" ? b : "day";
|
|
5821
|
+
function dashboardTime(db, f, bucketParam) {
|
|
5822
|
+
const w = sessionWhere(f);
|
|
5823
|
+
const mw = mainOnly(w);
|
|
5824
|
+
const uw = withModel(w, "t.model");
|
|
5825
|
+
const burn_hours = queryAll(db, `SELECT strftime('%Y-%m-%dT%H', e.timestamp) h, s.source_id src,
|
|
5826
|
+
SUM(t.input_tokens + t.output_tokens + t.cache_creation_input_tokens) work
|
|
5827
|
+
FROM token_usage t
|
|
5828
|
+
JOIN events e ON e.uuid = t.event_uuid
|
|
5829
|
+
JOIN sessions s ON s.id = t.session_id
|
|
5830
|
+
${uw.sql} GROUP BY h, src ORDER BY h`, ...uw.params).filter((r) => r.h != null).map((r) => ({ hour: r.h, source: r.src, work: r.work ?? 0 }));
|
|
5831
|
+
const tw = withModel(mw, "t.model");
|
|
5832
|
+
const turn_hours = queryAll(db, `SELECT strftime('%Y-%m-%dT%H', t.started_at) h, s.source_id src, COUNT(*) turns
|
|
5833
|
+
FROM turns t
|
|
5834
|
+
JOIN sessions s ON s.id = t.session_id
|
|
5835
|
+
${tw.sql} GROUP BY h, src ORDER BY h`, ...tw.params).filter((r) => r.h != null).map((r) => ({ hour: r.h, source: r.src, turns: r.turns ?? 0 }));
|
|
5836
|
+
return {
|
|
5837
|
+
burn_hours,
|
|
5838
|
+
turn_hours,
|
|
5839
|
+
latency: modelLatency(db, mw, bucketParam ? bucketFor(bucketParam) : chooseBucket(db, f)),
|
|
5840
|
+
review: reviewLatency(db, mw)
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
var MIN_LATENCY_SAMPLES = 5;
|
|
5844
|
+
function modelLatency(db, mw, bucket) {
|
|
5845
|
+
const expr = bucketExpr(bucket, "s.started_at");
|
|
5846
|
+
const lw = withModel(mw, "tn.model");
|
|
5847
|
+
const series = queryAll(db, `WITH lat AS MATERIALIZED (
|
|
5848
|
+
SELECT ${expr} b, COALESCE(${canonModelSql("tn.model")}, '(unknown)') model,
|
|
5849
|
+
(julianday(MIN(e.timestamp)) - julianday(tn.started_at)) * 86400000.0 ms
|
|
5850
|
+
FROM turns tn
|
|
5851
|
+
JOIN sessions s ON s.id = tn.session_id
|
|
5852
|
+
JOIN events e ON e.turn_id = tn.id AND e.role = 'assistant'
|
|
5853
|
+
${lw.sql} AND tn.started_at IS NOT NULL AND tn.model IS NOT '<synthetic>'
|
|
5854
|
+
GROUP BY tn.id
|
|
5855
|
+
),
|
|
5856
|
+
ranked AS (
|
|
5857
|
+
SELECT b, model, ms,
|
|
5858
|
+
ROW_NUMBER() OVER (PARTITION BY b, model ORDER BY ms) rn,
|
|
5859
|
+
COUNT(*) OVER (PARTITION BY b, model) c
|
|
5860
|
+
FROM lat WHERE ms IS NOT NULL AND ms >= 0
|
|
5861
|
+
)
|
|
5862
|
+
SELECT b, model, MAX(c) n,
|
|
5863
|
+
MAX(CASE WHEN rn = MAX(1, CAST(ceil(0.5 * c) AS INTEGER)) THEN ms END) p50,
|
|
5864
|
+
MAX(CASE WHEN rn = MAX(1, CAST(ceil(0.9 * c) AS INTEGER)) THEN ms END) p90
|
|
5865
|
+
FROM ranked GROUP BY b, model HAVING MAX(c) >= ${MIN_LATENCY_SAMPLES} ORDER BY b, model`, ...lw.params).filter((r) => r.b != null).map((r) => ({
|
|
5866
|
+
bucket: r.b,
|
|
5867
|
+
model: r.model ?? "(unknown)",
|
|
5868
|
+
p50_ms: Math.round(r.p50 ?? 0),
|
|
5869
|
+
p90_ms: Math.round(r.p90 ?? 0),
|
|
5870
|
+
n: r.n
|
|
5871
|
+
}));
|
|
5872
|
+
return { bucket, series };
|
|
5873
|
+
}
|
|
5874
|
+
function reviewLatency(db, mw) {
|
|
5875
|
+
const empty = () => ({ n: 0, under_10s: 0, under_30s: 0, under_2min: 0 });
|
|
5876
|
+
const out = { wrote: empty(), none: empty() };
|
|
5877
|
+
for (const r of queryAll(
|
|
5878
|
+
db,
|
|
5879
|
+
// `wrote` is resolved against a materialized set of turn ids rather than a correlated EXISTS:
|
|
5880
|
+
// file_changes has no index on turn_id, so the per-turn subquery rescanned the table once per
|
|
5881
|
+
// turn and cost 5.7s on the real corpus against 0.02s for this. Same counts either way.
|
|
5882
|
+
`WITH wrote_turns AS MATERIALIZED (SELECT DISTINCT turn_id FROM file_changes WHERE turn_id IS NOT NULL),
|
|
5883
|
+
turnaround AS MATERIALIZED (
|
|
5884
|
+
SELECT (tn.id IN (SELECT turn_id FROM wrote_turns)) wrote,
|
|
5885
|
+
(julianday(LEAD(tn.started_at) OVER (PARTITION BY tn.session_id ORDER BY tn.seq))
|
|
5886
|
+
- julianday(tn.ended_at)) * 86400000.0 d
|
|
5887
|
+
FROM turns tn JOIN sessions s ON s.id = tn.session_id ${mw.sql}
|
|
5888
|
+
)
|
|
5889
|
+
SELECT wrote, COUNT(*) n,
|
|
5890
|
+
SUM(CASE WHEN d <= 10000 THEN 1 ELSE 0 END) u10,
|
|
5891
|
+
SUM(CASE WHEN d <= 30000 THEN 1 ELSE 0 END) u30,
|
|
5892
|
+
SUM(CASE WHEN d <= 120000 THEN 1 ELSE 0 END) u120
|
|
5893
|
+
FROM turnaround WHERE d IS NOT NULL AND d >= 0 GROUP BY wrote`,
|
|
5894
|
+
...mw.params
|
|
5895
|
+
)) {
|
|
5896
|
+
out[r.wrote ? "wrote" : "none"] = { n: r.n, under_10s: r.u10 ?? 0, under_30s: r.u30 ?? 0, under_2min: r.u120 ?? 0 };
|
|
5897
|
+
}
|
|
5898
|
+
return out;
|
|
5899
|
+
}
|
|
5900
|
+
function dashboardAudit(db, f, bucketParam) {
|
|
5901
|
+
const w = sessionWhere(f);
|
|
5902
|
+
const bucket = bucketParam ? bucketFor(bucketParam) : chooseBucket(db, f);
|
|
5903
|
+
return {
|
|
5904
|
+
bucket,
|
|
5905
|
+
edit_reliability: editReliability(db, w),
|
|
5906
|
+
plan_rejections: planRejections(db, mainOnly(w), bucket),
|
|
5907
|
+
file_rework: fileRework(db, w),
|
|
5908
|
+
findings_over_time: findingsOverTime(db, w, bucket)
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
function editReliability(db, w) {
|
|
5912
|
+
const ew = withModel(w, "e.model");
|
|
5913
|
+
return queryAll(db, `SELECT COALESCE(${canonModelSql("e.model")}, '(unknown)') model, COUNT(*) calls,
|
|
5914
|
+
SUM(CASE WHEN tc.status = 'error' THEN 1 ELSE 0 END) errors
|
|
5915
|
+
FROM tool_calls tc ${eventJoin} JOIN sessions s ON s.id = tc.session_id
|
|
5916
|
+
${andWhere(ew)} tc.tool_name IN ('Edit', 'Write', 'NotebookEdit')
|
|
5917
|
+
GROUP BY 1 ORDER BY calls DESC`, ...ew.params);
|
|
5918
|
+
}
|
|
5919
|
+
function planRejections(db, mw, bucket) {
|
|
5920
|
+
const ew = withModel(mw, "e.model");
|
|
5921
|
+
const rejected = (tool) => `SUM(CASE WHEN tc.tool_name = '${tool}' AND tc.error_type = 'user-rejected' THEN 1 ELSE 0 END)`;
|
|
5922
|
+
const calls = (tool) => `SUM(CASE WHEN tc.tool_name = '${tool}' THEN 1 ELSE 0 END)`;
|
|
5923
|
+
return queryAll(db, `SELECT ${bucketExpr(bucket, "s.started_at")} bucket,
|
|
5924
|
+
${calls("ExitPlanMode")} plan_calls, ${rejected("ExitPlanMode")} plan_rejected,
|
|
5925
|
+
${calls("AskUserQuestion")} question_calls, ${rejected("AskUserQuestion")} question_rejected
|
|
5926
|
+
FROM tool_calls tc ${eventJoin} JOIN sessions s ON s.id = tc.session_id
|
|
5927
|
+
${andWhere(ew)} tc.tool_name IN ('ExitPlanMode', 'AskUserQuestion')
|
|
5928
|
+
GROUP BY bucket ORDER BY bucket`, ...ew.params).filter((r) => r.bucket != null);
|
|
5929
|
+
}
|
|
5930
|
+
var REWORK_BANDS = ["1", "2-3", "4-6", "7-12", "13+"];
|
|
5931
|
+
function fileRework(db, w) {
|
|
5932
|
+
const ew = withModel(w, "e.model");
|
|
5933
|
+
const rows = queryAll(db, `WITH pairs AS (
|
|
5934
|
+
SELECT COUNT(*) n
|
|
5935
|
+
FROM file_changes fc
|
|
5936
|
+
JOIN events e ON e.uuid = fc.event_uuid
|
|
5937
|
+
JOIN sessions s ON s.id = fc.session_id
|
|
5938
|
+
${ew.sql} GROUP BY fc.session_id, fc.file_path
|
|
5939
|
+
)
|
|
5940
|
+
SELECT CASE WHEN n = 1 THEN '1' WHEN n <= 3 THEN '2-3' WHEN n <= 6 THEN '4-6'
|
|
5941
|
+
WHEN n <= 12 THEN '7-12' ELSE '13+' END band,
|
|
5942
|
+
COUNT(*) pairs, SUM(n) changes
|
|
5943
|
+
FROM pairs GROUP BY band`, ...ew.params);
|
|
5944
|
+
const byBand = new Map(rows.map((r) => [r.band, r]));
|
|
5945
|
+
return REWORK_BANDS.map((band2) => byBand.get(band2) ?? { band: band2, pairs: 0, changes: 0 });
|
|
5946
|
+
}
|
|
5947
|
+
function findingsOverTime(db, w, bucket) {
|
|
5948
|
+
const ew = withModel(w, "e.model");
|
|
5949
|
+
const sev = (s) => `SUM(CASE WHEN f.severity = '${s}' THEN 1 ELSE 0 END) ${s}`;
|
|
5950
|
+
return queryAll(db, `SELECT ${bucketExpr(bucket, "e.timestamp")} bucket,
|
|
5951
|
+
${sev("info")}, ${sev("low")}, ${sev("medium")}, ${sev("high")}, ${sev("critical")}
|
|
5952
|
+
FROM findings f
|
|
5953
|
+
JOIN events e ON e.uuid = f.event_uuid
|
|
5954
|
+
JOIN sessions s ON s.id = f.session_id
|
|
5955
|
+
${ew.sql} GROUP BY bucket ORDER BY bucket`, ...ew.params).filter((r) => r.bucket != null);
|
|
5956
|
+
}
|
|
5626
5957
|
|
|
5627
5958
|
// ../server/dist/refresh.js
|
|
5628
5959
|
import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "node:fs";
|
|
@@ -5894,7 +6225,7 @@ async function createApp(db, opts = {}) {
|
|
|
5894
6225
|
app.get("/api/models", async () => listModels(db));
|
|
5895
6226
|
const dashFilters = (req) => {
|
|
5896
6227
|
const q = req.query;
|
|
5897
|
-
return { source: q.source, from: q.from, to: q.to };
|
|
6228
|
+
return { source: q.source, from: q.from, to: q.to, models: q.models?.split(",").filter(Boolean) };
|
|
5898
6229
|
};
|
|
5899
6230
|
app.get("/api/dashboard/overview", async (req) => dashboardOverview(db, dashFilters(req)));
|
|
5900
6231
|
app.get("/api/dashboard/timeseries", async (req) => {
|
|
@@ -5902,6 +6233,14 @@ async function createApp(db, opts = {}) {
|
|
|
5902
6233
|
return dashboardTimeseries(db, dashFilters(req), q.bucket);
|
|
5903
6234
|
});
|
|
5904
6235
|
app.get("/api/dashboard/breakdowns", async (req) => dashboardBreakdowns(db, dashFilters(req)));
|
|
6236
|
+
app.get("/api/dashboard/time", async (req) => {
|
|
6237
|
+
const q = req.query;
|
|
6238
|
+
return dashboardTime(db, dashFilters(req), q.bucket);
|
|
6239
|
+
});
|
|
6240
|
+
app.get("/api/dashboard/audit", async (req) => {
|
|
6241
|
+
const q = req.query;
|
|
6242
|
+
return dashboardAudit(db, dashFilters(req), q.bucket);
|
|
6243
|
+
});
|
|
5905
6244
|
const PREF_KEY = /^[a-z0-9._-]{1,64}$/i;
|
|
5906
6245
|
app.get("/api/prefs/:key", async (req, reply) => {
|
|
5907
6246
|
const { key } = req.params;
|
|
@@ -6251,8 +6590,8 @@ cli.command("export <sessionId>", "Write a session to a shareable Markdown file
|
|
|
6251
6590
|
cli.command(
|
|
6252
6591
|
"service <action> [target]",
|
|
6253
6592
|
"Install/uninstall/status OS services \u2014 action: install|uninstall|status; target: collector|server|all (default all)"
|
|
6254
|
-
).option("--times <hours>", "Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)").action((action,
|
|
6255
|
-
const targets = parseTargets(
|
|
6593
|
+
).option("--times <hours>", "Collector cadence: comma-separated hours 0-23 (default: 9,13,17,21)").action((action, target2, opts) => {
|
|
6594
|
+
const targets = parseTargets(target2);
|
|
6256
6595
|
const hours = action === "install" ? parseHours(opts.times) : void 0;
|
|
6257
6596
|
runService(action, { cliEntry: fileURLToPath4(import.meta.url), hours, targets });
|
|
6258
6597
|
});
|