@swestash/agent-lens 0.1.0 → 0.3.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 +573 -51
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/CopyButton-DPveQcsm.js +1 -0
- package/web/assets/{Dashboard-CPjOOCyK.js → Dashboard-D50k35sH.js} +1 -1
- package/web/assets/SessionView-DPm1eauh.js +6 -0
- package/web/assets/{SkillView-C6vlyg0Z.js → SkillView-ByiQB_SI.js} +1 -1
- package/web/assets/SkillsView-lOJiFu6e.js +1 -0
- package/web/assets/WorkflowView-qgx0vvWs.js +1 -0
- package/web/assets/index-CU4_1QLm.css +1 -0
- package/web/assets/index-CWdoCK2Q.js +68 -0
- package/web/assets/{index-RKLKBrv5.js → index-eVzr9g61.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/SessionView-iEtAsqjR.js +0 -2
- package/web/assets/SkillsView-B2W0OtAw.js +0 -1
- package/web/assets/WorkflowView-D5N-tAZr.js +0 -1
- package/web/assets/index-B0Y86jYR.css +0 -1
- package/web/assets/index-DaWyEmDk.js +0 -68
package/dist/agent-lens.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
5
|
-
import { dirname as dirname6, join as
|
|
4
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
7
|
|
|
8
8
|
// ../../node_modules/.pnpm/cac@6.7.14/node_modules/cac/dist/index.mjs
|
|
@@ -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 = 11;
|
|
595
595
|
var SCHEMA_SQL = (
|
|
596
596
|
/* sql */
|
|
597
597
|
`
|
|
@@ -781,6 +781,61 @@ CREATE TABLE IF NOT EXISTS classifications (
|
|
|
781
781
|
PRIMARY KEY (scope, target_id)
|
|
782
782
|
);
|
|
783
783
|
|
|
784
|
+
-- Workflow-tool run results, ingested from the sidecar JSON the runner writes next to the launching
|
|
785
|
+
-- session (\u2026/projects/<enc>/<sessionId>/workflows/wf_<id>.json). The transcript only carries the
|
|
786
|
+
-- "launched in background" ack for an async run; this sidecar is the authoritative record of how the
|
|
787
|
+
-- run finished \u2014 status (completed/failed), summary, the returned result payload, and roll-up
|
|
788
|
+
-- tokens/tool-calls/duration/agent-count. One row per run id; UPSERT on re-ingest. Stored verbatim
|
|
789
|
+
-- (like tool_calls.input_json) \u2014 the DB is local; redaction is an export-time concern. --------------
|
|
790
|
+
CREATE TABLE IF NOT EXISTS workflow_results (
|
|
791
|
+
run_id TEXT PRIMARY KEY, -- wf_<id> (matches tool_calls.workflow_run_id / sessions.workflow_run_id)
|
|
792
|
+
source_id TEXT, -- which configured source (account) it came from
|
|
793
|
+
session_id TEXT, -- launching session (path segment before /workflows/)
|
|
794
|
+
task_id TEXT, -- async task id (the "Task ID: \u2026" in the launch ack)
|
|
795
|
+
workflow_name TEXT,
|
|
796
|
+
status TEXT, -- completed | failed | running | ...
|
|
797
|
+
summary TEXT,
|
|
798
|
+
default_model TEXT, -- model the run executed on (e.g. claude-fable-5)
|
|
799
|
+
result_json TEXT, -- the returned result object, verbatim JSON (null when none)
|
|
800
|
+
phases_json TEXT, -- [{title}] the run's phase structure (e.g. Generate \u2192 Judge)
|
|
801
|
+
logs_json TEXT, -- concise per-item outcome lines the runner emitted
|
|
802
|
+
progress_json TEXT, -- workflowProgress: the phase/agent event timeline
|
|
803
|
+
agent_count INTEGER, -- agents the run reports having fanned out to
|
|
804
|
+
total_tokens INTEGER,
|
|
805
|
+
total_tool_calls INTEGER,
|
|
806
|
+
duration_ms INTEGER,
|
|
807
|
+
started_at TEXT, -- ISO, from the sidecar startTime/timestamp
|
|
808
|
+
ended_at TEXT, -- ISO, started_at + duration
|
|
809
|
+
ingested_at TEXT
|
|
810
|
+
);
|
|
811
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_results_session ON workflow_results(session_id);
|
|
812
|
+
|
|
813
|
+
-- Per-subagent metadata sidecar (subagents/agent-<id>.meta.json). One row per subagent session,
|
|
814
|
+
-- joined at read time onto sessions (LEFT JOIN, may precede the session row's arrival). Supplies the
|
|
815
|
+
-- authoritative agentType/description/spawnDepth \u2014 Workflow fan-out subagents carry none in-transcript.
|
|
816
|
+
CREATE TABLE IF NOT EXISTS session_meta (
|
|
817
|
+
session_id TEXT PRIMARY KEY, -- 'agent-<id>' = the subagent session id (meta filename stem)
|
|
818
|
+
source_id TEXT,
|
|
819
|
+
agent_type TEXT, -- meta.agentType (e.g. 'Explore', 'claude-code-guide')
|
|
820
|
+
agent_description TEXT, -- meta.description (a real human title)
|
|
821
|
+
spawn_depth INTEGER, -- meta.spawnDepth (optional; absent \u21D2 top-level)
|
|
822
|
+
tool_use_id TEXT, -- meta.toolUseId (the spawning tool_call)
|
|
823
|
+
ingested_at TEXT
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
-- Full un-truncated tool outputs spilled to disk (tool-results/<name>.txt) when a result was too large
|
|
827
|
+
-- for the transcript, which keeps only a 280-char summary naming the file. Joined at read time by
|
|
828
|
+
-- parsing that summary's '\u2026/tool-results/<name>.txt' marker back to (session_id, name).
|
|
829
|
+
CREATE TABLE IF NOT EXISTS tool_results (
|
|
830
|
+
session_id TEXT, -- session the tool ran in (from the archive path)
|
|
831
|
+
name TEXT, -- file basename without .txt (matches the marker in tool_calls.result_summary)
|
|
832
|
+
path TEXT, -- archive path (provenance)
|
|
833
|
+
bytes INTEGER, -- original size, for the UI
|
|
834
|
+
text TEXT, -- full un-truncated output, stored inline (self-contained DB)
|
|
835
|
+
ingested_at TEXT,
|
|
836
|
+
PRIMARY KEY (session_id, name)
|
|
837
|
+
);
|
|
838
|
+
|
|
784
839
|
-- Idempotent ingest bookkeeping per archive file. --------------------------
|
|
785
840
|
CREATE TABLE IF NOT EXISTS ingest_state (
|
|
786
841
|
file_path TEXT PRIMARY KEY,
|
|
@@ -1148,16 +1203,17 @@ function snapshot(absPath, relParts, versionsDir, stats) {
|
|
|
1148
1203
|
syncMeta(dst, statSync(absPath));
|
|
1149
1204
|
stats.snapshots++;
|
|
1150
1205
|
}
|
|
1151
|
-
function
|
|
1206
|
+
function walkCollectable(dir, out, inWorkflows = false, inToolResults = false) {
|
|
1152
1207
|
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
1153
1208
|
const p = join2(dir, e.name);
|
|
1154
|
-
if (e.isDirectory())
|
|
1155
|
-
|
|
1156
|
-
else if (e.isFile() && e.name.endsWith(".jsonl"))
|
|
1209
|
+
if (e.isDirectory()) {
|
|
1210
|
+
walkCollectable(p, out, inWorkflows || e.name === "workflows", inToolResults || e.name === "tool-results");
|
|
1211
|
+
} else if (e.isFile() && (e.name.endsWith(".jsonl") || inWorkflows || inToolResults || e.name.endsWith(".meta.json"))) {
|
|
1157
1212
|
out.push(p);
|
|
1213
|
+
}
|
|
1158
1214
|
}
|
|
1159
1215
|
}
|
|
1160
|
-
function syncFile(src, dst, relParts, archive, versionsDir, stats) {
|
|
1216
|
+
function syncFile(src, dst, relParts, archive, versionsDir, stats, appendable) {
|
|
1161
1217
|
const srcStat = statSync(src);
|
|
1162
1218
|
if (!existsSync2(dst)) {
|
|
1163
1219
|
copyWhole(src, dst, srcStat);
|
|
@@ -1169,11 +1225,11 @@ function syncFile(src, dst, relParts, archive, versionsDir, stats) {
|
|
|
1169
1225
|
return;
|
|
1170
1226
|
const ssize = srcStat.size;
|
|
1171
1227
|
const asize = arcStat.size;
|
|
1172
|
-
if (ssize
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1228
|
+
if (ssize === asize && prefixEquals(src, dst, asize)) {
|
|
1229
|
+
syncMeta(dst, srcStat);
|
|
1230
|
+
} else if (appendable && ssize > asize && prefixEquals(src, dst, asize)) {
|
|
1231
|
+
appendRange(src, dst, asize, ssize);
|
|
1232
|
+
stats.appended++;
|
|
1177
1233
|
syncMeta(dst, srcStat);
|
|
1178
1234
|
} else if (ssize < asize) {
|
|
1179
1235
|
const last = latestSnapshot(archive, relParts);
|
|
@@ -1214,7 +1270,7 @@ function collectSource(source, archiveBase, excludedDirs, runTs, stats, log) {
|
|
|
1214
1270
|
const files = [];
|
|
1215
1271
|
const projectsDir = join2(configDir, "projects");
|
|
1216
1272
|
if (existsSync2(projectsDir))
|
|
1217
|
-
|
|
1273
|
+
walkCollectable(projectsDir, files);
|
|
1218
1274
|
const historySrc = join2(configDir, "history.jsonl");
|
|
1219
1275
|
if (existsSync2(historySrc))
|
|
1220
1276
|
files.push(historySrc);
|
|
@@ -1228,7 +1284,7 @@ function collectSource(source, archiveBase, excludedDirs, runTs, stats, log) {
|
|
|
1228
1284
|
if (excludedDirs.some((enc) => relPosix.startsWith(`projects/${enc}/`)))
|
|
1229
1285
|
continue;
|
|
1230
1286
|
stats.scanned++;
|
|
1231
|
-
syncFile(src, join2(archive, ...relParts), relParts, archive, versionsDir, stats);
|
|
1287
|
+
syncFile(src, join2(archive, ...relParts), relParts, archive, versionsDir, stats, base.endsWith(".jsonl"));
|
|
1232
1288
|
}
|
|
1233
1289
|
for (const f of ["settings.json", "settings.local.json"]) {
|
|
1234
1290
|
const s = join2(configDir, f);
|
|
@@ -1627,8 +1683,8 @@ function runService(action, params) {
|
|
|
1627
1683
|
}
|
|
1628
1684
|
|
|
1629
1685
|
// ../ingest/dist/run.js
|
|
1630
|
-
import { readFileSync as
|
|
1631
|
-
import { dirname as dirname4, join as
|
|
1686
|
+
import { readFileSync as readFileSync7, statSync as statSync5, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
|
|
1687
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
1632
1688
|
|
|
1633
1689
|
// ../ingest/dist/db.js
|
|
1634
1690
|
import Database from "better-sqlite3";
|
|
@@ -1638,9 +1694,16 @@ function openRaw(file) {
|
|
|
1638
1694
|
db.pragma("foreign_keys = ON");
|
|
1639
1695
|
return db;
|
|
1640
1696
|
}
|
|
1697
|
+
function readSchemaVersion(db) {
|
|
1698
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
1699
|
+
if (!row)
|
|
1700
|
+
return null;
|
|
1701
|
+
const n = Number(row.value);
|
|
1702
|
+
return Number.isFinite(n) ? n : null;
|
|
1703
|
+
}
|
|
1641
1704
|
function applySchema(db) {
|
|
1642
1705
|
db.exec(SCHEMA_SQL);
|
|
1643
|
-
db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO
|
|
1706
|
+
db.prepare("INSERT INTO meta(key, value) VALUES ('schema_version', ?) ON CONFLICT(key) DO NOTHING").run(String(SCHEMA_VERSION));
|
|
1644
1707
|
}
|
|
1645
1708
|
function openDb(file) {
|
|
1646
1709
|
const db = openRaw(file);
|
|
@@ -1664,6 +1727,9 @@ function resetSchema(db) {
|
|
|
1664
1727
|
DROP TABLE IF EXISTS projects;
|
|
1665
1728
|
DROP TABLE IF EXISTS sources;
|
|
1666
1729
|
DROP TABLE IF EXISTS agents;
|
|
1730
|
+
DROP TABLE IF EXISTS workflow_results;
|
|
1731
|
+
DROP TABLE IF EXISTS session_meta;
|
|
1732
|
+
DROP TABLE IF EXISTS tool_results;
|
|
1667
1733
|
DROP TABLE IF EXISTS ingest_state;
|
|
1668
1734
|
DROP TABLE IF EXISTS meta;
|
|
1669
1735
|
`);
|
|
@@ -1875,9 +1941,10 @@ var ClaudeCodeAdapter = class {
|
|
|
1875
1941
|
result_summary: null
|
|
1876
1942
|
});
|
|
1877
1943
|
} else if (b.type === "tool_result" && asString(b.tool_use_id)) {
|
|
1944
|
+
const isAskUser = tur && tur.questions && tur.answers && typeof tur.answers === "object";
|
|
1878
1945
|
toolResults.push({
|
|
1879
1946
|
tool_use_id: b.tool_use_id,
|
|
1880
|
-
result_summary: summarizeResult(b.content),
|
|
1947
|
+
result_summary: isAskUser ? JSON.stringify({ answers: tur.answers, annotations: tur.annotations ?? {} }) : summarizeResult(b.content),
|
|
1881
1948
|
...tur ? {
|
|
1882
1949
|
status: asString(tur.status),
|
|
1883
1950
|
agent_type: asString(tur.agentType),
|
|
@@ -2607,6 +2674,10 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now, stats) {
|
|
|
2607
2674
|
tx();
|
|
2608
2675
|
}
|
|
2609
2676
|
|
|
2677
|
+
// ../ingest/dist/workflows.js
|
|
2678
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
2679
|
+
import { basename as basename3, join as join5 } from "path";
|
|
2680
|
+
|
|
2610
2681
|
// ../ingest/dist/redact.js
|
|
2611
2682
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
2612
2683
|
function parseExcludes(csv) {
|
|
@@ -2661,6 +2732,334 @@ function* streamLines(path) {
|
|
|
2661
2732
|
}
|
|
2662
2733
|
}
|
|
2663
2734
|
|
|
2735
|
+
// ../ingest/dist/workflows.js
|
|
2736
|
+
function newWorkflowStats() {
|
|
2737
|
+
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
2738
|
+
}
|
|
2739
|
+
function walkSidecars(dir, inWorkflows, out) {
|
|
2740
|
+
let entries;
|
|
2741
|
+
try {
|
|
2742
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
2743
|
+
} catch {
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
for (const e of entries) {
|
|
2747
|
+
const p = join5(dir, e.name);
|
|
2748
|
+
if (e.isDirectory())
|
|
2749
|
+
walkSidecars(p, inWorkflows || e.name === "workflows", out);
|
|
2750
|
+
else if (e.isFile() && inWorkflows && /^wf_.+\.json$/.test(e.name))
|
|
2751
|
+
out.push(p);
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
function discoverSidecars(sourceArchiveDir) {
|
|
2755
|
+
const out = [];
|
|
2756
|
+
walkSidecars(join5(sourceArchiveDir, "projects"), false, out);
|
|
2757
|
+
try {
|
|
2758
|
+
for (const ts of readdirSync3(join5(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
2759
|
+
if (ts.isDirectory())
|
|
2760
|
+
walkSidecars(join5(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
2761
|
+
} catch {
|
|
2762
|
+
}
|
|
2763
|
+
return out;
|
|
2764
|
+
}
|
|
2765
|
+
function sessionFromPath(path) {
|
|
2766
|
+
const parts = path.split("/");
|
|
2767
|
+
const i = parts.indexOf("workflows");
|
|
2768
|
+
return i > 0 ? parts[i - 1] : null;
|
|
2769
|
+
}
|
|
2770
|
+
function toIso(x) {
|
|
2771
|
+
if (typeof x === "number" && Number.isFinite(x))
|
|
2772
|
+
return new Date(x).toISOString();
|
|
2773
|
+
if (typeof x === "string" && x) {
|
|
2774
|
+
const t = Date.parse(x);
|
|
2775
|
+
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
2776
|
+
}
|
|
2777
|
+
return null;
|
|
2778
|
+
}
|
|
2779
|
+
var jsonOrNull = (v) => v == null ? null : JSON.stringify(v);
|
|
2780
|
+
var intOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
|
|
2781
|
+
var strOrNull = (v) => typeof v === "string" && v ? v : null;
|
|
2782
|
+
function cleanStr(v) {
|
|
2783
|
+
const s = strOrNull(v);
|
|
2784
|
+
if (!s)
|
|
2785
|
+
return null;
|
|
2786
|
+
return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\[[0-9;]+m\]?/g, "").trim() || null;
|
|
2787
|
+
}
|
|
2788
|
+
function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now, stats, full) {
|
|
2789
|
+
let paths = discoverSidecars(sourceArchiveDir);
|
|
2790
|
+
if (excludedDirs.length)
|
|
2791
|
+
paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
|
|
2792
|
+
paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
|
|
2793
|
+
const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
|
|
2794
|
+
const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
|
|
2795
|
+
VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
|
|
2796
|
+
ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
|
|
2797
|
+
const upsert = db.prepare(`INSERT INTO workflow_results
|
|
2798
|
+
(run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,
|
|
2799
|
+
result_json, phases_json, logs_json, progress_json,
|
|
2800
|
+
agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)
|
|
2801
|
+
VALUES
|
|
2802
|
+
(@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,
|
|
2803
|
+
@result_json, @phases_json, @logs_json, @progress_json,
|
|
2804
|
+
@agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)
|
|
2805
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
2806
|
+
source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,
|
|
2807
|
+
workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,
|
|
2808
|
+
default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,
|
|
2809
|
+
logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,
|
|
2810
|
+
total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
|
|
2811
|
+
duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
2812
|
+
ingested_at=excluded.ingested_at`);
|
|
2813
|
+
for (const path of paths) {
|
|
2814
|
+
stats.files++;
|
|
2815
|
+
const st = statSync2(path);
|
|
2816
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2817
|
+
const prev = full ? void 0 : getState.get(path);
|
|
2818
|
+
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
2819
|
+
stats.skipped++;
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
let buf;
|
|
2823
|
+
try {
|
|
2824
|
+
buf = readFileSync4(path);
|
|
2825
|
+
} catch {
|
|
2826
|
+
stats.malformed++;
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
const hash = sha256(buf);
|
|
2830
|
+
if (prev && prev.sha256 === hash) {
|
|
2831
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2832
|
+
stats.skipped++;
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
let d;
|
|
2836
|
+
try {
|
|
2837
|
+
d = JSON.parse(buf.toString("utf8"));
|
|
2838
|
+
} catch {
|
|
2839
|
+
stats.malformed++;
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (!d || typeof d !== "object") {
|
|
2843
|
+
stats.malformed++;
|
|
2844
|
+
continue;
|
|
2845
|
+
}
|
|
2846
|
+
const runId = strOrNull(d.runId) ?? basename3(path, ".json");
|
|
2847
|
+
const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
|
|
2848
|
+
const durationMs2 = intOrNull(d.durationMs);
|
|
2849
|
+
const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
|
|
2850
|
+
upsert.run({
|
|
2851
|
+
run_id: runId,
|
|
2852
|
+
source_id: sourceId,
|
|
2853
|
+
session_id: sessionFromPath(path),
|
|
2854
|
+
task_id: strOrNull(d.taskId),
|
|
2855
|
+
workflow_name: strOrNull(d.workflowName),
|
|
2856
|
+
status: strOrNull(d.status),
|
|
2857
|
+
summary: strOrNull(d.summary),
|
|
2858
|
+
default_model: cleanStr(d.defaultModel),
|
|
2859
|
+
result_json: jsonOrNull(d.result),
|
|
2860
|
+
phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,
|
|
2861
|
+
logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,
|
|
2862
|
+
progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,
|
|
2863
|
+
agent_count: intOrNull(d.agentCount),
|
|
2864
|
+
total_tokens: intOrNull(d.totalTokens),
|
|
2865
|
+
total_tool_calls: intOrNull(d.totalToolCalls),
|
|
2866
|
+
duration_ms: durationMs2,
|
|
2867
|
+
started_at: startedAt,
|
|
2868
|
+
ended_at: endedAt,
|
|
2869
|
+
ingested_at: now
|
|
2870
|
+
});
|
|
2871
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2872
|
+
stats.upserted++;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
// ../ingest/dist/meta.js
|
|
2877
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
2878
|
+
import { basename as basename4, join as join6 } from "path";
|
|
2879
|
+
function newMetaStats() {
|
|
2880
|
+
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
2881
|
+
}
|
|
2882
|
+
function walkMeta(dir, inSubagents, out) {
|
|
2883
|
+
let entries;
|
|
2884
|
+
try {
|
|
2885
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2886
|
+
} catch {
|
|
2887
|
+
return;
|
|
2888
|
+
}
|
|
2889
|
+
for (const e of entries) {
|
|
2890
|
+
const p = join6(dir, e.name);
|
|
2891
|
+
if (e.isDirectory())
|
|
2892
|
+
walkMeta(p, inSubagents || e.name === "subagents", out);
|
|
2893
|
+
else if (e.isFile() && inSubagents && /^agent-.+\.meta\.json$/.test(e.name))
|
|
2894
|
+
out.push(p);
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
function discoverMeta(sourceArchiveDir) {
|
|
2898
|
+
const out = [];
|
|
2899
|
+
walkMeta(join6(sourceArchiveDir, "projects"), false, out);
|
|
2900
|
+
try {
|
|
2901
|
+
for (const ts of readdirSync4(join6(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
2902
|
+
if (ts.isDirectory())
|
|
2903
|
+
walkMeta(join6(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
2904
|
+
} catch {
|
|
2905
|
+
}
|
|
2906
|
+
return out;
|
|
2907
|
+
}
|
|
2908
|
+
var intOrNull2 = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
|
|
2909
|
+
var strOrNull2 = (v) => typeof v === "string" && v ? v : null;
|
|
2910
|
+
function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now, stats, full) {
|
|
2911
|
+
let paths = discoverMeta(sourceArchiveDir);
|
|
2912
|
+
if (excludedDirs.length)
|
|
2913
|
+
paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
|
|
2914
|
+
paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
|
|
2915
|
+
const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
|
|
2916
|
+
const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
|
|
2917
|
+
VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
|
|
2918
|
+
ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
|
|
2919
|
+
const upsert = db.prepare(`INSERT INTO session_meta
|
|
2920
|
+
(session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)
|
|
2921
|
+
VALUES
|
|
2922
|
+
(@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)
|
|
2923
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
2924
|
+
source_id=excluded.source_id, agent_type=excluded.agent_type,
|
|
2925
|
+
agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
|
|
2926
|
+
tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
|
|
2927
|
+
for (const path of paths) {
|
|
2928
|
+
stats.files++;
|
|
2929
|
+
const st = statSync3(path);
|
|
2930
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2931
|
+
const prev = full ? void 0 : getState.get(path);
|
|
2932
|
+
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
2933
|
+
stats.skipped++;
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
let buf;
|
|
2937
|
+
try {
|
|
2938
|
+
buf = readFileSync5(path);
|
|
2939
|
+
} catch {
|
|
2940
|
+
stats.malformed++;
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
const hash = sha256(buf);
|
|
2944
|
+
if (prev && prev.sha256 === hash) {
|
|
2945
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2946
|
+
stats.skipped++;
|
|
2947
|
+
continue;
|
|
2948
|
+
}
|
|
2949
|
+
let d;
|
|
2950
|
+
try {
|
|
2951
|
+
d = JSON.parse(buf.toString("utf8"));
|
|
2952
|
+
} catch {
|
|
2953
|
+
stats.malformed++;
|
|
2954
|
+
continue;
|
|
2955
|
+
}
|
|
2956
|
+
if (!d || typeof d !== "object") {
|
|
2957
|
+
stats.malformed++;
|
|
2958
|
+
continue;
|
|
2959
|
+
}
|
|
2960
|
+
const sessionId = basename4(path, ".meta.json");
|
|
2961
|
+
upsert.run({
|
|
2962
|
+
session_id: sessionId,
|
|
2963
|
+
source_id: sourceId,
|
|
2964
|
+
agent_type: strOrNull2(d.agentType),
|
|
2965
|
+
agent_description: strOrNull2(d.description),
|
|
2966
|
+
spawn_depth: intOrNull2(d.spawnDepth),
|
|
2967
|
+
tool_use_id: strOrNull2(d.toolUseId),
|
|
2968
|
+
ingested_at: now
|
|
2969
|
+
});
|
|
2970
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2971
|
+
stats.upserted++;
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
|
|
2975
|
+
// ../ingest/dist/toolResults.js
|
|
2976
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
2977
|
+
import { basename as basename5, join as join7 } from "path";
|
|
2978
|
+
function newToolResultStats() {
|
|
2979
|
+
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
2980
|
+
}
|
|
2981
|
+
function walkResults(dir, inResults, out) {
|
|
2982
|
+
let entries;
|
|
2983
|
+
try {
|
|
2984
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
2985
|
+
} catch {
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
for (const e of entries) {
|
|
2989
|
+
const p = join7(dir, e.name);
|
|
2990
|
+
if (e.isDirectory())
|
|
2991
|
+
walkResults(p, inResults || e.name === "tool-results", out);
|
|
2992
|
+
else if (e.isFile() && inResults && e.name.endsWith(".txt"))
|
|
2993
|
+
out.push(p);
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
function discoverResults(sourceArchiveDir) {
|
|
2997
|
+
const out = [];
|
|
2998
|
+
walkResults(join7(sourceArchiveDir, "projects"), false, out);
|
|
2999
|
+
try {
|
|
3000
|
+
for (const ts of readdirSync5(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3001
|
+
if (ts.isDirectory())
|
|
3002
|
+
walkResults(join7(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3003
|
+
} catch {
|
|
3004
|
+
}
|
|
3005
|
+
return out;
|
|
3006
|
+
}
|
|
3007
|
+
function sessionFromPath2(path) {
|
|
3008
|
+
const parts = path.split("/");
|
|
3009
|
+
const i = parts.indexOf("tool-results");
|
|
3010
|
+
return i > 0 ? parts[i - 1] : null;
|
|
3011
|
+
}
|
|
3012
|
+
function ingestToolResults(db, sourceArchiveDir, excludedDirs, now, stats, full) {
|
|
3013
|
+
let paths = discoverResults(sourceArchiveDir);
|
|
3014
|
+
if (excludedDirs.length)
|
|
3015
|
+
paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
|
|
3016
|
+
paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
|
|
3017
|
+
const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
|
|
3018
|
+
const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
|
|
3019
|
+
VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
|
|
3020
|
+
ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
|
|
3021
|
+
const upsert = db.prepare(`INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)
|
|
3022
|
+
VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
|
|
3023
|
+
ON CONFLICT(session_id, name) DO UPDATE SET
|
|
3024
|
+
path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
|
|
3025
|
+
for (const path of paths) {
|
|
3026
|
+
const sessionId = sessionFromPath2(path);
|
|
3027
|
+
if (!sessionId)
|
|
3028
|
+
continue;
|
|
3029
|
+
stats.files++;
|
|
3030
|
+
const st = statSync4(path);
|
|
3031
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3032
|
+
const prev = full ? void 0 : getState.get(path);
|
|
3033
|
+
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3034
|
+
stats.skipped++;
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
let buf;
|
|
3038
|
+
try {
|
|
3039
|
+
buf = readFileSync6(path);
|
|
3040
|
+
} catch {
|
|
3041
|
+
stats.malformed++;
|
|
3042
|
+
continue;
|
|
3043
|
+
}
|
|
3044
|
+
const hash = sha256(buf);
|
|
3045
|
+
if (prev && prev.sha256 === hash) {
|
|
3046
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
3047
|
+
stats.skipped++;
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
upsert.run({
|
|
3051
|
+
session_id: sessionId,
|
|
3052
|
+
name: basename5(path, ".txt"),
|
|
3053
|
+
path,
|
|
3054
|
+
bytes: st.size,
|
|
3055
|
+
text: buf.toString("utf8"),
|
|
3056
|
+
ingested_at: now
|
|
3057
|
+
});
|
|
3058
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
3059
|
+
stats.upserted++;
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2664
3063
|
// ../ingest/dist/run.js
|
|
2665
3064
|
function parseIngestArgs(argv) {
|
|
2666
3065
|
const a = { full: false, db: "", archive: "" };
|
|
@@ -2677,16 +3076,24 @@ function parseIngestArgs(argv) {
|
|
|
2677
3076
|
function runIngest(argv = process.argv.slice(2)) {
|
|
2678
3077
|
const args = parseIngestArgs(argv);
|
|
2679
3078
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
2680
|
-
const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE ||
|
|
2681
|
-
const dbPath = args.db || process.env.AGENT_LENS_DB ||
|
|
3079
|
+
const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE || join8(dataDir, "archive");
|
|
3080
|
+
const dbPath = args.db || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
|
|
2682
3081
|
if (!existsSync4(archiveRoot)) {
|
|
2683
3082
|
console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
|
|
2684
3083
|
process.exit(1);
|
|
2685
3084
|
}
|
|
2686
3085
|
mkdirSync4(dirname4(dbPath), { recursive: true });
|
|
2687
3086
|
const db = args.full ? openRaw(dbPath) : openDb(dbPath);
|
|
2688
|
-
if (args.full)
|
|
3087
|
+
if (args.full) {
|
|
2689
3088
|
resetSchema(db);
|
|
3089
|
+
} else {
|
|
3090
|
+
const dbVer = readSchemaVersion(db);
|
|
3091
|
+
if (dbVer != null && dbVer !== SCHEMA_VERSION) {
|
|
3092
|
+
console.error(`agent-lens-ingest: schema mismatch \u2014 DB is v${dbVer}, this build expects v${SCHEMA_VERSION}. Run 'agent-lens ingest --full' to rebuild.`);
|
|
3093
|
+
db.close();
|
|
3094
|
+
process.exit(1);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
2690
3097
|
const adapterList = [new ClaudeCodeAdapter()];
|
|
2691
3098
|
const adapterById = new Map(adapterList.map((a) => [a.agentId, a]));
|
|
2692
3099
|
const sources = loadSources();
|
|
@@ -2696,6 +3103,9 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2696
3103
|
const pruned = pruneExcluded(db, excludePaths);
|
|
2697
3104
|
const stmts = prepareStatements(db);
|
|
2698
3105
|
const stats = newStats();
|
|
3106
|
+
const wfStats = newWorkflowStats();
|
|
3107
|
+
const metaStats = newMetaStats();
|
|
3108
|
+
const trStats = newToolResultStats();
|
|
2699
3109
|
const dirty = /* @__PURE__ */ new Set();
|
|
2700
3110
|
for (const source of sources) {
|
|
2701
3111
|
const adapter = adapterById.get(source.agent);
|
|
@@ -2705,13 +3115,13 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2705
3115
|
}
|
|
2706
3116
|
stmts.insAgent.run(adapter.agentId, adapter.agentName);
|
|
2707
3117
|
stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
|
|
2708
|
-
let files = adapter.discover(
|
|
3118
|
+
let files = adapter.discover(join8(archiveRoot, source.label), source.label);
|
|
2709
3119
|
if (excludedDirs.length)
|
|
2710
3120
|
files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
|
|
2711
3121
|
files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
|
|
2712
3122
|
for (const file of files) {
|
|
2713
3123
|
stats.files++;
|
|
2714
|
-
const st =
|
|
3124
|
+
const st = statSync5(file.path);
|
|
2715
3125
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2716
3126
|
const prev = args.full ? void 0 : stmts.getState.get(file.path);
|
|
2717
3127
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
@@ -2719,7 +3129,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2719
3129
|
continue;
|
|
2720
3130
|
}
|
|
2721
3131
|
const small = st.size <= STREAM_THRESHOLD;
|
|
2722
|
-
const buf = small ?
|
|
3132
|
+
const buf = small ? readFileSync7(file.path) : null;
|
|
2723
3133
|
const hash = small ? sha256(buf) : sha256File(file.path);
|
|
2724
3134
|
if (prev && prev.sha256 === hash) {
|
|
2725
3135
|
stmts.setState.run({
|
|
@@ -2737,6 +3147,9 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2737
3147
|
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash }, now, stats);
|
|
2738
3148
|
dirty.add(file.sessionId);
|
|
2739
3149
|
}
|
|
3150
|
+
ingestWorkflowResults(db, join8(archiveRoot, source.label), source.label, excludedDirs, now, wfStats, args.full);
|
|
3151
|
+
ingestSubagentMeta(db, join8(archiveRoot, source.label), source.label, excludedDirs, now, metaStats, args.full);
|
|
3152
|
+
ingestToolResults(db, join8(archiveRoot, source.label), excludedDirs, now, trStats, args.full);
|
|
2740
3153
|
}
|
|
2741
3154
|
const expanded = rebuildDerived(db, args.full ? null : dirty);
|
|
2742
3155
|
const classified = classify(db, args.full ? null : expanded);
|
|
@@ -2752,9 +3165,15 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2752
3165
|
cost += costForUsage(u.model, u);
|
|
2753
3166
|
totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
|
|
2754
3167
|
}
|
|
3168
|
+
const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
|
|
3169
|
+
const metaRows = count("SELECT COUNT(*) n FROM session_meta");
|
|
3170
|
+
const trRows = count("SELECT COUNT(*) n FROM tool_results");
|
|
2755
3171
|
db.close();
|
|
2756
3172
|
console.log(`agent-lens-ingest: files=${stats.files} skipped=${stats.skipped} new_events=${stats.newEvents} malformed=${stats.malformed}${pruned ? ` excluded_pruned=${pruned}` : ""}
|
|
2757
3173
|
sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${classified.count}
|
|
3174
|
+
workflow_results=${wfRuns} (sidecar upserted=${wfStats.upserted} skipped=${wfStats.skipped} malformed=${wfStats.malformed})
|
|
3175
|
+
session_meta=${metaRows} (upserted=${metaStats.upserted} skipped=${metaStats.skipped} malformed=${metaStats.malformed})
|
|
3176
|
+
tool_results=${trRows} (upserted=${trStats.upserted} skipped=${trStats.skipped} malformed=${trStats.malformed})
|
|
2758
3177
|
tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
|
|
2759
3178
|
}
|
|
2760
3179
|
function runMetrics(argv = process.argv.slice(2)) {
|
|
@@ -2763,7 +3182,7 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
2763
3182
|
if (argv[i] === "--db")
|
|
2764
3183
|
dbArg = argv[++i] ?? "";
|
|
2765
3184
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
2766
|
-
const dbPath = dbArg || process.env.AGENT_LENS_DB ||
|
|
3185
|
+
const dbPath = dbArg || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
|
|
2767
3186
|
if (!existsSync4(dbPath)) {
|
|
2768
3187
|
console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);
|
|
2769
3188
|
process.exit(1);
|
|
@@ -2776,7 +3195,7 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
2776
3195
|
|
|
2777
3196
|
// ../server/dist/run.js
|
|
2778
3197
|
import { existsSync as existsSync7 } from "fs";
|
|
2779
|
-
import { dirname as dirname5, join as
|
|
3198
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2780
3199
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2781
3200
|
|
|
2782
3201
|
// ../server/dist/db.js
|
|
@@ -2786,6 +3205,12 @@ function openReadonly(file) {
|
|
|
2786
3205
|
db.pragma("query_only = ON");
|
|
2787
3206
|
return db;
|
|
2788
3207
|
}
|
|
3208
|
+
function schemaStatus(db) {
|
|
3209
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
3210
|
+
const dbVersion = row ? Number(row.value) : null;
|
|
3211
|
+
const db_version = dbVersion != null && Number.isFinite(dbVersion) ? dbVersion : null;
|
|
3212
|
+
return { db_version, expected: SCHEMA_VERSION, stale: db_version != null && db_version !== SCHEMA_VERSION };
|
|
3213
|
+
}
|
|
2789
3214
|
function lastIngested(db) {
|
|
2790
3215
|
const row = db.prepare("SELECT MAX(ingested_at) AS last FROM ingest_state").get();
|
|
2791
3216
|
return row?.last ?? null;
|
|
@@ -2858,21 +3283,47 @@ function listSessions(db, f) {
|
|
|
2858
3283
|
}
|
|
2859
3284
|
const whereSql = where.length ? `WHERE ${where.join(" AND ")}` : "";
|
|
2860
3285
|
const total = db.prepare(`SELECT COUNT(*) n FROM sessions s ${whereSql}`).get(...params).n;
|
|
2861
|
-
const
|
|
3286
|
+
const baseSelect = `SELECT s.id, s.ai_title, s.slug, s.source_id, s.is_sidechain, s.started_at, s.ended_at,
|
|
2862
3287
|
s.duration_ms, s.event_count, s.turn_count, p.path AS project_path,
|
|
2863
3288
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
2864
3289
|
FROM sessions s LEFT JOIN projects p ON p.id = s.project_id
|
|
2865
|
-
${whereSql}
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
3290
|
+
${whereSql}`;
|
|
3291
|
+
const NATIVE_ORDER = {
|
|
3292
|
+
started: "s.started_at",
|
|
3293
|
+
title: "COALESCE(s.ai_title, s.slug)",
|
|
3294
|
+
turns: "s.turn_count",
|
|
3295
|
+
duration: "s.duration_ms"
|
|
3296
|
+
};
|
|
3297
|
+
const sortKey = f.sort ?? "started";
|
|
3298
|
+
const sqlDir = f.dir === "asc" ? "ASC" : "DESC";
|
|
3299
|
+
let rows;
|
|
3300
|
+
if (sortKey === "tokens" || sortKey === "cost") {
|
|
3301
|
+
const all = db.prepare(baseSelect).all(...params);
|
|
3302
|
+
attachSessionCost(db, all);
|
|
3303
|
+
const sign = f.dir === "asc" ? 1 : -1;
|
|
3304
|
+
all.sort((a, b) => {
|
|
3305
|
+
const c = (sortKey === "tokens" ? a.tokens - b.tokens : a.cost - b.cost) * sign;
|
|
3306
|
+
return c !== 0 ? c : String(a.id).localeCompare(String(b.id));
|
|
3307
|
+
});
|
|
3308
|
+
rows = all.slice(f.offset, f.offset + f.limit);
|
|
3309
|
+
} else {
|
|
3310
|
+
const orderCol = NATIVE_ORDER[sortKey] ?? NATIVE_ORDER.started;
|
|
3311
|
+
rows = db.prepare(`${baseSelect} ORDER BY ${orderCol} ${sqlDir}, s.id ASC LIMIT ? OFFSET ?`).all(...params, f.limit, f.offset);
|
|
3312
|
+
attachSessionCost(db, rows);
|
|
3313
|
+
}
|
|
3314
|
+
return { total, sessions: rows };
|
|
3315
|
+
}
|
|
3316
|
+
function attachSessionCost(db, rows) {
|
|
2869
3317
|
const costBySession = /* @__PURE__ */ new Map();
|
|
2870
|
-
|
|
2871
|
-
|
|
3318
|
+
const ids = rows.map((r) => r.id);
|
|
3319
|
+
const CHUNK3 = 800;
|
|
3320
|
+
for (let i = 0; i < ids.length; i += CHUNK3) {
|
|
3321
|
+
const batch = ids.slice(i, i + CHUNK3);
|
|
3322
|
+
const ph = batch.map(() => "?").join(",");
|
|
2872
3323
|
const usage = db.prepare(`SELECT session_id, model,
|
|
2873
3324
|
SUM(input_tokens) i, SUM(output_tokens) o,
|
|
2874
3325
|
SUM(cache_creation_input_tokens) cw, SUM(cache_read_input_tokens) cr
|
|
2875
|
-
FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`).all(...
|
|
3326
|
+
FROM token_usage WHERE session_id IN (${ph}) GROUP BY session_id, model`).all(...batch);
|
|
2876
3327
|
for (const u of usage) {
|
|
2877
3328
|
const acc = costBySession.get(u.session_id) ?? { tokens: 0, cost: 0, split: { input: 0, output: 0, cache_creation: 0, cache_read: 0 } };
|
|
2878
3329
|
acc.tokens += u.i + u.o + u.cw + u.cr;
|
|
@@ -2896,7 +3347,6 @@ function listSessions(db, f) {
|
|
|
2896
3347
|
r.cost = c ? Number(c.cost.toFixed(4)) : 0;
|
|
2897
3348
|
r.title = r.ai_title || r.slug || null;
|
|
2898
3349
|
}
|
|
2899
|
-
return { total, sessions: rows };
|
|
2900
3350
|
}
|
|
2901
3351
|
function getSession(db, id) {
|
|
2902
3352
|
const session = db.prepare(`SELECT s.*, p.path AS project_path FROM sessions s
|
|
@@ -2911,6 +3361,17 @@ function getSession(db, id) {
|
|
|
2911
3361
|
total_duration_ms, total_tokens, input_json, result_summary,
|
|
2912
3362
|
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
|
|
2913
3363
|
FROM tool_calls WHERE session_id = ?`).all(id);
|
|
3364
|
+
if (tableExists(db, "tool_results")) {
|
|
3365
|
+
const getFull = db.prepare("SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?");
|
|
3366
|
+
for (const t of toolRows) {
|
|
3367
|
+
const m = typeof t.result_summary === "string" ? t.result_summary.match(/tool-results\/([A-Za-z0-9_-]+)\.txt/) : null;
|
|
3368
|
+
if (!m)
|
|
3369
|
+
continue;
|
|
3370
|
+
const full = getFull.get(id, m[1]);
|
|
3371
|
+
if (full)
|
|
3372
|
+
t.full_result = { text: full.text, bytes: full.bytes };
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
2914
3375
|
const toolsByEvent = /* @__PURE__ */ new Map();
|
|
2915
3376
|
for (const t of toolRows) {
|
|
2916
3377
|
if (!t.event_uuid)
|
|
@@ -2978,9 +3439,12 @@ function getSession(db, id) {
|
|
|
2978
3439
|
if (p)
|
|
2979
3440
|
parent = { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null };
|
|
2980
3441
|
}
|
|
3442
|
+
const hasMeta = tableExists(db, "session_meta");
|
|
2981
3443
|
const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
|
|
2982
3444
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
2983
|
-
|
|
3445
|
+
${hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth"}
|
|
3446
|
+
FROM sessions s ${hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : ""}
|
|
3447
|
+
WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
|
|
2984
3448
|
for (const ch of children) {
|
|
2985
3449
|
ch.title = ch.ai_title || ch.slug || null;
|
|
2986
3450
|
const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
|
|
@@ -2989,8 +3453,10 @@ function getSession(db, id) {
|
|
|
2989
3453
|
ch.tokens = u.reduce((a, r) => a + r.i + r.o + r.cw + r.cr, 0);
|
|
2990
3454
|
ch.cost = Number(u.reduce((a, r) => a + costForUsage(r.model, { input_tokens: r.i, output_tokens: r.o, cache_creation_input_tokens: r.cw, cache_read_input_tokens: r.cr }), 0).toFixed(4));
|
|
2991
3455
|
}
|
|
3456
|
+
const hasWR = tableExists(db, "workflow_results");
|
|
2992
3457
|
const workflow_runs = db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
|
|
2993
3458
|
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
|
|
3459
|
+
${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
|
|
2994
3460
|
FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
|
|
2995
3461
|
WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
|
|
2996
3462
|
ORDER BY t.seq`).all(id);
|
|
@@ -2999,8 +3465,21 @@ function getSession(db, id) {
|
|
|
2999
3465
|
function xmlTag(text, tag) {
|
|
3000
3466
|
return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
|
|
3001
3467
|
}
|
|
3468
|
+
function tableExists(db, name) {
|
|
3469
|
+
return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
|
|
3470
|
+
}
|
|
3471
|
+
function safeJson(s) {
|
|
3472
|
+
if (!s)
|
|
3473
|
+
return null;
|
|
3474
|
+
try {
|
|
3475
|
+
return JSON.parse(s);
|
|
3476
|
+
} catch {
|
|
3477
|
+
return null;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3002
3480
|
function getWorkflow(db, runId) {
|
|
3003
3481
|
const wf = db.prepare(`SELECT tc.id AS tool_use_id, tc.workflow_run_id AS run_id, tc.workflow_name AS name, tc.status, tc.result_summary,
|
|
3482
|
+
tc.input_json AS input_json,
|
|
3004
3483
|
tc.session_id AS parent_session_id, t.seq AS turn_seq,
|
|
3005
3484
|
ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
|
|
3006
3485
|
FROM tool_calls tc
|
|
@@ -3011,9 +3490,12 @@ function getWorkflow(db, runId) {
|
|
|
3011
3490
|
LIMIT 1`).get(runId);
|
|
3012
3491
|
if (!wf)
|
|
3013
3492
|
return null;
|
|
3493
|
+
const hasMeta = tableExists(db, "session_meta");
|
|
3014
3494
|
const agents = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.ended_at, s.duration_ms,
|
|
3015
3495
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
3016
|
-
|
|
3496
|
+
${hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth"}
|
|
3497
|
+
FROM sessions s ${hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : ""}
|
|
3498
|
+
WHERE s.workflow_run_id = ? ORDER BY s.started_at`).all(runId);
|
|
3017
3499
|
let total_tokens = 0;
|
|
3018
3500
|
let total_cost = 0;
|
|
3019
3501
|
let started_at = null;
|
|
@@ -3044,12 +3526,44 @@ function getWorkflow(db, runId) {
|
|
|
3044
3526
|
completion = { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
|
|
3045
3527
|
}
|
|
3046
3528
|
}
|
|
3529
|
+
const wr = tableExists(db, "workflow_results") ? db.prepare(`SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
|
|
3530
|
+
agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
|
|
3531
|
+
FROM workflow_results WHERE run_id = ?`).get(wf.run_id) : null;
|
|
3532
|
+
let run = null;
|
|
3533
|
+
if (wr) {
|
|
3534
|
+
run = {
|
|
3535
|
+
status: wr.status ?? null,
|
|
3536
|
+
summary: wr.summary ?? null,
|
|
3537
|
+
default_model: wr.default_model ?? null,
|
|
3538
|
+
agent_count: wr.agent_count ?? null,
|
|
3539
|
+
total_tokens: wr.total_tokens ?? null,
|
|
3540
|
+
total_tool_calls: wr.total_tool_calls ?? null,
|
|
3541
|
+
duration_ms: wr.duration_ms ?? null,
|
|
3542
|
+
started_at: wr.started_at ?? null,
|
|
3543
|
+
ended_at: wr.ended_at ?? null,
|
|
3544
|
+
phases: safeJson(wr.phases_json),
|
|
3545
|
+
logs: safeJson(wr.logs_json),
|
|
3546
|
+
// The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
|
|
3547
|
+
// with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.
|
|
3548
|
+
progress: safeJson(wr.progress_json)
|
|
3549
|
+
};
|
|
3550
|
+
if (wr.result_json != null || wr.status) {
|
|
3551
|
+
completion = {
|
|
3552
|
+
status: wr.status ?? completion?.status ?? null,
|
|
3553
|
+
summary: wr.summary ?? completion?.summary ?? null,
|
|
3554
|
+
result: wr.result_json ?? completion?.result ?? null,
|
|
3555
|
+
failures: completion?.failures ?? null
|
|
3556
|
+
};
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3047
3559
|
return {
|
|
3048
3560
|
run_id: wf.run_id,
|
|
3049
3561
|
name: wf.name ?? null,
|
|
3050
|
-
status: wf.status ?? null,
|
|
3562
|
+
status: wr?.status ?? wf.status ?? null,
|
|
3051
3563
|
result_summary: wf.result_summary ?? null,
|
|
3564
|
+
input_json: wf.input_json ?? null,
|
|
3052
3565
|
completion,
|
|
3566
|
+
run,
|
|
3053
3567
|
parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },
|
|
3054
3568
|
agents,
|
|
3055
3569
|
stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
|
|
@@ -3058,8 +3572,11 @@ function getWorkflow(db, runId) {
|
|
|
3058
3572
|
function listSkills(db, f = {}) {
|
|
3059
3573
|
const where = ["tc.tool_name = 'Skill'", "tc.skill_name IS NOT NULL"];
|
|
3060
3574
|
const params = [];
|
|
3061
|
-
if (f.q && f.q.trim())
|
|
3062
|
-
|
|
3575
|
+
if (f.q && f.q.trim()) {
|
|
3576
|
+
const like = `%${f.q.trim()}%`;
|
|
3577
|
+
where.push("(tc.skill_name LIKE ? OR EXISTS (SELECT 1 FROM skills sk WHERE sk.name = tc.skill_name AND sk.body LIKE ?))");
|
|
3578
|
+
params.push(like, like);
|
|
3579
|
+
}
|
|
3063
3580
|
if (f.source)
|
|
3064
3581
|
where.push("s.source_id = ?"), params.push(f.source);
|
|
3065
3582
|
if (f.project)
|
|
@@ -3276,7 +3793,7 @@ function dashboardBreakdowns(db, f) {
|
|
|
3276
3793
|
|
|
3277
3794
|
// ../server/dist/refresh.js
|
|
3278
3795
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
3279
|
-
import { join as
|
|
3796
|
+
import { join as join9 } from "path";
|
|
3280
3797
|
function originAllowed(origin) {
|
|
3281
3798
|
if (!origin)
|
|
3282
3799
|
return true;
|
|
@@ -3289,12 +3806,12 @@ function originAllowed(origin) {
|
|
|
3289
3806
|
}
|
|
3290
3807
|
function runRefresh() {
|
|
3291
3808
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
3292
|
-
const lock = acquireLock(
|
|
3809
|
+
const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
|
|
3293
3810
|
if (!lock)
|
|
3294
3811
|
return null;
|
|
3295
3812
|
try {
|
|
3296
3813
|
const collected = collectAll();
|
|
3297
|
-
const archiveRoot = process.env.AGENT_LENS_ARCHIVE ||
|
|
3814
|
+
const archiveRoot = process.env.AGENT_LENS_ARCHIVE || join9(dataDir, "archive");
|
|
3298
3815
|
if (!existsSync5(archiveRoot))
|
|
3299
3816
|
mkdirSync5(archiveRoot, { recursive: true });
|
|
3300
3817
|
runIngest([]);
|
|
@@ -3307,7 +3824,10 @@ function runRefresh() {
|
|
|
3307
3824
|
// ../server/dist/app.js
|
|
3308
3825
|
async function createApp(db, opts = {}) {
|
|
3309
3826
|
const app = Fastify({ logger: false });
|
|
3310
|
-
app.get("/api/health", async () =>
|
|
3827
|
+
app.get("/api/health", async () => {
|
|
3828
|
+
const s = schemaStatus(db);
|
|
3829
|
+
return { ok: true, last_ingested: lastIngested(db), schema_version: s.db_version, schema_stale: s.stale };
|
|
3830
|
+
});
|
|
3311
3831
|
app.post("/api/refresh", async (req, reply) => {
|
|
3312
3832
|
if (!originAllowed(req.headers.origin)) {
|
|
3313
3833
|
return reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin refresh blocked" } });
|
|
@@ -3346,6 +3866,8 @@ async function createApp(db, opts = {}) {
|
|
|
3346
3866
|
from: q.from,
|
|
3347
3867
|
to: q.to,
|
|
3348
3868
|
kind: q.kind === "main" || q.kind === "subagent" ? q.kind : void 0,
|
|
3869
|
+
sort: ["started", "title", "turns", "tokens", "cost", "duration"].find((s) => s === q.sort),
|
|
3870
|
+
dir: q.dir === "asc" ? "asc" : q.dir === "desc" ? "desc" : void 0,
|
|
3349
3871
|
limit: Math.min(Number(q.limit) || 50, 200),
|
|
3350
3872
|
offset: Number(q.offset) || 0
|
|
3351
3873
|
});
|
|
@@ -3421,7 +3943,7 @@ async function createApp(db, opts = {}) {
|
|
|
3421
3943
|
async function startServer() {
|
|
3422
3944
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
3423
3945
|
const repoRoot = findRepoRoot(here);
|
|
3424
|
-
const dbPath = process.env.AGENT_LENS_DB ||
|
|
3946
|
+
const dbPath = process.env.AGENT_LENS_DB || join10(resolveDataDir(repoRoot), "agent-lens.db");
|
|
3425
3947
|
const host = process.env.AGENT_LENS_HOST || "127.0.0.1";
|
|
3426
3948
|
const port = Number(process.env.AGENT_LENS_PORT || 4477);
|
|
3427
3949
|
const webDist = resolveWebDist(here, repoRoot);
|
|
@@ -3448,11 +3970,11 @@ async function startServer() {
|
|
|
3448
3970
|
|
|
3449
3971
|
// src/watch.ts
|
|
3450
3972
|
import { existsSync as existsSync8 } from "fs";
|
|
3451
|
-
import { join as
|
|
3973
|
+
import { join as join11 } from "path";
|
|
3452
3974
|
import chokidar from "chokidar";
|
|
3453
3975
|
async function runWatch(opts = {}) {
|
|
3454
3976
|
const sources = loadSources();
|
|
3455
|
-
const lockPath =
|
|
3977
|
+
const lockPath = join11(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
3456
3978
|
let running = false;
|
|
3457
3979
|
let pending = false;
|
|
3458
3980
|
const cycle = () => {
|
|
@@ -3485,7 +4007,7 @@ async function runWatch(opts = {}) {
|
|
|
3485
4007
|
}
|
|
3486
4008
|
};
|
|
3487
4009
|
cycle();
|
|
3488
|
-
const watchPaths = sources.map((s) =>
|
|
4010
|
+
const watchPaths = sources.map((s) => join11(s.configDir, "projects")).filter((p) => existsSync8(p));
|
|
3489
4011
|
if (watchPaths.length === 0) {
|
|
3490
4012
|
console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
|
|
3491
4013
|
}
|
|
@@ -3513,14 +4035,14 @@ async function runWatch(opts = {}) {
|
|
|
3513
4035
|
// src/index.ts
|
|
3514
4036
|
function version() {
|
|
3515
4037
|
try {
|
|
3516
|
-
const pkg = JSON.parse(
|
|
4038
|
+
const pkg = JSON.parse(readFileSync8(join12(dirname6(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
|
|
3517
4039
|
return pkg.version ?? "0.0.0";
|
|
3518
4040
|
} catch {
|
|
3519
4041
|
return "0.0.0";
|
|
3520
4042
|
}
|
|
3521
4043
|
}
|
|
3522
4044
|
function withLock(fn) {
|
|
3523
|
-
const lockPath =
|
|
4045
|
+
const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
3524
4046
|
const lock = acquireLock(lockPath);
|
|
3525
4047
|
if (!lock) {
|
|
3526
4048
|
console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");
|