@swestash/agent-lens 0.2.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 +530 -39
- 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-BxZigTdE.js → Dashboard-D50k35sH.js} +1 -1
- package/web/assets/SessionView-DPm1eauh.js +6 -0
- package/web/assets/{SkillView-Zs7vZQPU.js → SkillView-ByiQB_SI.js} +1 -1
- package/web/assets/{SkillsView-5RaEi7vd.js → SkillsView-lOJiFu6e.js} +1 -1
- package/web/assets/WorkflowView-qgx0vvWs.js +1 -0
- package/web/assets/index-CU4_1QLm.css +1 -0
- package/web/assets/{index-CH9ukarY.js → index-CWdoCK2Q.js} +12 -12
- package/web/assets/{index-753CcEbN.js → index-eVzr9g61.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/SessionView-DW8drCh_.js +0 -4
- package/web/assets/WorkflowView-BZgguHyQ.js +0 -1
- package/web/assets/index-CtWea52P.css +0 -1
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
|
`);
|
|
@@ -2608,6 +2674,10 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now, stats) {
|
|
|
2608
2674
|
tx();
|
|
2609
2675
|
}
|
|
2610
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
|
+
|
|
2611
2681
|
// ../ingest/dist/redact.js
|
|
2612
2682
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
2613
2683
|
function parseExcludes(csv) {
|
|
@@ -2662,6 +2732,334 @@ function* streamLines(path) {
|
|
|
2662
2732
|
}
|
|
2663
2733
|
}
|
|
2664
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
|
+
|
|
2665
3063
|
// ../ingest/dist/run.js
|
|
2666
3064
|
function parseIngestArgs(argv) {
|
|
2667
3065
|
const a = { full: false, db: "", archive: "" };
|
|
@@ -2678,16 +3076,24 @@ function parseIngestArgs(argv) {
|
|
|
2678
3076
|
function runIngest(argv = process.argv.slice(2)) {
|
|
2679
3077
|
const args = parseIngestArgs(argv);
|
|
2680
3078
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
2681
|
-
const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE ||
|
|
2682
|
-
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");
|
|
2683
3081
|
if (!existsSync4(archiveRoot)) {
|
|
2684
3082
|
console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
|
|
2685
3083
|
process.exit(1);
|
|
2686
3084
|
}
|
|
2687
3085
|
mkdirSync4(dirname4(dbPath), { recursive: true });
|
|
2688
3086
|
const db = args.full ? openRaw(dbPath) : openDb(dbPath);
|
|
2689
|
-
if (args.full)
|
|
3087
|
+
if (args.full) {
|
|
2690
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
|
+
}
|
|
2691
3097
|
const adapterList = [new ClaudeCodeAdapter()];
|
|
2692
3098
|
const adapterById = new Map(adapterList.map((a) => [a.agentId, a]));
|
|
2693
3099
|
const sources = loadSources();
|
|
@@ -2697,6 +3103,9 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2697
3103
|
const pruned = pruneExcluded(db, excludePaths);
|
|
2698
3104
|
const stmts = prepareStatements(db);
|
|
2699
3105
|
const stats = newStats();
|
|
3106
|
+
const wfStats = newWorkflowStats();
|
|
3107
|
+
const metaStats = newMetaStats();
|
|
3108
|
+
const trStats = newToolResultStats();
|
|
2700
3109
|
const dirty = /* @__PURE__ */ new Set();
|
|
2701
3110
|
for (const source of sources) {
|
|
2702
3111
|
const adapter = adapterById.get(source.agent);
|
|
@@ -2706,13 +3115,13 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2706
3115
|
}
|
|
2707
3116
|
stmts.insAgent.run(adapter.agentId, adapter.agentName);
|
|
2708
3117
|
stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
|
|
2709
|
-
let files = adapter.discover(
|
|
3118
|
+
let files = adapter.discover(join8(archiveRoot, source.label), source.label);
|
|
2710
3119
|
if (excludedDirs.length)
|
|
2711
3120
|
files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
|
|
2712
3121
|
files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
|
|
2713
3122
|
for (const file of files) {
|
|
2714
3123
|
stats.files++;
|
|
2715
|
-
const st =
|
|
3124
|
+
const st = statSync5(file.path);
|
|
2716
3125
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2717
3126
|
const prev = args.full ? void 0 : stmts.getState.get(file.path);
|
|
2718
3127
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
@@ -2720,7 +3129,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2720
3129
|
continue;
|
|
2721
3130
|
}
|
|
2722
3131
|
const small = st.size <= STREAM_THRESHOLD;
|
|
2723
|
-
const buf = small ?
|
|
3132
|
+
const buf = small ? readFileSync7(file.path) : null;
|
|
2724
3133
|
const hash = small ? sha256(buf) : sha256File(file.path);
|
|
2725
3134
|
if (prev && prev.sha256 === hash) {
|
|
2726
3135
|
stmts.setState.run({
|
|
@@ -2738,6 +3147,9 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2738
3147
|
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash }, now, stats);
|
|
2739
3148
|
dirty.add(file.sessionId);
|
|
2740
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);
|
|
2741
3153
|
}
|
|
2742
3154
|
const expanded = rebuildDerived(db, args.full ? null : dirty);
|
|
2743
3155
|
const classified = classify(db, args.full ? null : expanded);
|
|
@@ -2753,9 +3165,15 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2753
3165
|
cost += costForUsage(u.model, u);
|
|
2754
3166
|
totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
|
|
2755
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");
|
|
2756
3171
|
db.close();
|
|
2757
3172
|
console.log(`agent-lens-ingest: files=${stats.files} skipped=${stats.skipped} new_events=${stats.newEvents} malformed=${stats.malformed}${pruned ? ` excluded_pruned=${pruned}` : ""}
|
|
2758
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})
|
|
2759
3177
|
tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
|
|
2760
3178
|
}
|
|
2761
3179
|
function runMetrics(argv = process.argv.slice(2)) {
|
|
@@ -2764,7 +3182,7 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
2764
3182
|
if (argv[i] === "--db")
|
|
2765
3183
|
dbArg = argv[++i] ?? "";
|
|
2766
3184
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
2767
|
-
const dbPath = dbArg || process.env.AGENT_LENS_DB ||
|
|
3185
|
+
const dbPath = dbArg || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
|
|
2768
3186
|
if (!existsSync4(dbPath)) {
|
|
2769
3187
|
console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);
|
|
2770
3188
|
process.exit(1);
|
|
@@ -2777,7 +3195,7 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
2777
3195
|
|
|
2778
3196
|
// ../server/dist/run.js
|
|
2779
3197
|
import { existsSync as existsSync7 } from "fs";
|
|
2780
|
-
import { dirname as dirname5, join as
|
|
3198
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2781
3199
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2782
3200
|
|
|
2783
3201
|
// ../server/dist/db.js
|
|
@@ -2787,6 +3205,12 @@ function openReadonly(file) {
|
|
|
2787
3205
|
db.pragma("query_only = ON");
|
|
2788
3206
|
return db;
|
|
2789
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
|
+
}
|
|
2790
3214
|
function lastIngested(db) {
|
|
2791
3215
|
const row = db.prepare("SELECT MAX(ingested_at) AS last FROM ingest_state").get();
|
|
2792
3216
|
return row?.last ?? null;
|
|
@@ -2937,6 +3361,17 @@ function getSession(db, id) {
|
|
|
2937
3361
|
total_duration_ms, total_tokens, input_json, result_summary,
|
|
2938
3362
|
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
|
|
2939
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
|
+
}
|
|
2940
3375
|
const toolsByEvent = /* @__PURE__ */ new Map();
|
|
2941
3376
|
for (const t of toolRows) {
|
|
2942
3377
|
if (!t.event_uuid)
|
|
@@ -3004,9 +3439,12 @@ function getSession(db, id) {
|
|
|
3004
3439
|
if (p)
|
|
3005
3440
|
parent = { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null };
|
|
3006
3441
|
}
|
|
3442
|
+
const hasMeta = tableExists(db, "session_meta");
|
|
3007
3443
|
const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
|
|
3008
3444
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
3009
|
-
|
|
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);
|
|
3010
3448
|
for (const ch of children) {
|
|
3011
3449
|
ch.title = ch.ai_title || ch.slug || null;
|
|
3012
3450
|
const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
|
|
@@ -3015,8 +3453,10 @@ function getSession(db, id) {
|
|
|
3015
3453
|
ch.tokens = u.reduce((a, r) => a + r.i + r.o + r.cw + r.cr, 0);
|
|
3016
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));
|
|
3017
3455
|
}
|
|
3456
|
+
const hasWR = tableExists(db, "workflow_results");
|
|
3018
3457
|
const workflow_runs = db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
|
|
3019
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"}
|
|
3020
3460
|
FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
|
|
3021
3461
|
WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
|
|
3022
3462
|
ORDER BY t.seq`).all(id);
|
|
@@ -3025,8 +3465,21 @@ function getSession(db, id) {
|
|
|
3025
3465
|
function xmlTag(text, tag) {
|
|
3026
3466
|
return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
|
|
3027
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
|
+
}
|
|
3028
3480
|
function getWorkflow(db, runId) {
|
|
3029
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,
|
|
3030
3483
|
tc.session_id AS parent_session_id, t.seq AS turn_seq,
|
|
3031
3484
|
ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
|
|
3032
3485
|
FROM tool_calls tc
|
|
@@ -3037,9 +3490,12 @@ function getWorkflow(db, runId) {
|
|
|
3037
3490
|
LIMIT 1`).get(runId);
|
|
3038
3491
|
if (!wf)
|
|
3039
3492
|
return null;
|
|
3493
|
+
const hasMeta = tableExists(db, "session_meta");
|
|
3040
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,
|
|
3041
3495
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
3042
|
-
|
|
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);
|
|
3043
3499
|
let total_tokens = 0;
|
|
3044
3500
|
let total_cost = 0;
|
|
3045
3501
|
let started_at = null;
|
|
@@ -3070,12 +3526,44 @@ function getWorkflow(db, runId) {
|
|
|
3070
3526
|
completion = { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
|
|
3071
3527
|
}
|
|
3072
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
|
+
}
|
|
3073
3559
|
return {
|
|
3074
3560
|
run_id: wf.run_id,
|
|
3075
3561
|
name: wf.name ?? null,
|
|
3076
|
-
status: wf.status ?? null,
|
|
3562
|
+
status: wr?.status ?? wf.status ?? null,
|
|
3077
3563
|
result_summary: wf.result_summary ?? null,
|
|
3564
|
+
input_json: wf.input_json ?? null,
|
|
3078
3565
|
completion,
|
|
3566
|
+
run,
|
|
3079
3567
|
parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },
|
|
3080
3568
|
agents,
|
|
3081
3569
|
stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
|
|
@@ -3305,7 +3793,7 @@ function dashboardBreakdowns(db, f) {
|
|
|
3305
3793
|
|
|
3306
3794
|
// ../server/dist/refresh.js
|
|
3307
3795
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
3308
|
-
import { join as
|
|
3796
|
+
import { join as join9 } from "path";
|
|
3309
3797
|
function originAllowed(origin) {
|
|
3310
3798
|
if (!origin)
|
|
3311
3799
|
return true;
|
|
@@ -3318,12 +3806,12 @@ function originAllowed(origin) {
|
|
|
3318
3806
|
}
|
|
3319
3807
|
function runRefresh() {
|
|
3320
3808
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
3321
|
-
const lock = acquireLock(
|
|
3809
|
+
const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
|
|
3322
3810
|
if (!lock)
|
|
3323
3811
|
return null;
|
|
3324
3812
|
try {
|
|
3325
3813
|
const collected = collectAll();
|
|
3326
|
-
const archiveRoot = process.env.AGENT_LENS_ARCHIVE ||
|
|
3814
|
+
const archiveRoot = process.env.AGENT_LENS_ARCHIVE || join9(dataDir, "archive");
|
|
3327
3815
|
if (!existsSync5(archiveRoot))
|
|
3328
3816
|
mkdirSync5(archiveRoot, { recursive: true });
|
|
3329
3817
|
runIngest([]);
|
|
@@ -3336,7 +3824,10 @@ function runRefresh() {
|
|
|
3336
3824
|
// ../server/dist/app.js
|
|
3337
3825
|
async function createApp(db, opts = {}) {
|
|
3338
3826
|
const app = Fastify({ logger: false });
|
|
3339
|
-
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
|
+
});
|
|
3340
3831
|
app.post("/api/refresh", async (req, reply) => {
|
|
3341
3832
|
if (!originAllowed(req.headers.origin)) {
|
|
3342
3833
|
return reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin refresh blocked" } });
|
|
@@ -3452,7 +3943,7 @@ async function createApp(db, opts = {}) {
|
|
|
3452
3943
|
async function startServer() {
|
|
3453
3944
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
3454
3945
|
const repoRoot = findRepoRoot(here);
|
|
3455
|
-
const dbPath = process.env.AGENT_LENS_DB ||
|
|
3946
|
+
const dbPath = process.env.AGENT_LENS_DB || join10(resolveDataDir(repoRoot), "agent-lens.db");
|
|
3456
3947
|
const host = process.env.AGENT_LENS_HOST || "127.0.0.1";
|
|
3457
3948
|
const port = Number(process.env.AGENT_LENS_PORT || 4477);
|
|
3458
3949
|
const webDist = resolveWebDist(here, repoRoot);
|
|
@@ -3479,11 +3970,11 @@ async function startServer() {
|
|
|
3479
3970
|
|
|
3480
3971
|
// src/watch.ts
|
|
3481
3972
|
import { existsSync as existsSync8 } from "fs";
|
|
3482
|
-
import { join as
|
|
3973
|
+
import { join as join11 } from "path";
|
|
3483
3974
|
import chokidar from "chokidar";
|
|
3484
3975
|
async function runWatch(opts = {}) {
|
|
3485
3976
|
const sources = loadSources();
|
|
3486
|
-
const lockPath =
|
|
3977
|
+
const lockPath = join11(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
3487
3978
|
let running = false;
|
|
3488
3979
|
let pending = false;
|
|
3489
3980
|
const cycle = () => {
|
|
@@ -3516,7 +4007,7 @@ async function runWatch(opts = {}) {
|
|
|
3516
4007
|
}
|
|
3517
4008
|
};
|
|
3518
4009
|
cycle();
|
|
3519
|
-
const watchPaths = sources.map((s) =>
|
|
4010
|
+
const watchPaths = sources.map((s) => join11(s.configDir, "projects")).filter((p) => existsSync8(p));
|
|
3520
4011
|
if (watchPaths.length === 0) {
|
|
3521
4012
|
console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
|
|
3522
4013
|
}
|
|
@@ -3544,14 +4035,14 @@ async function runWatch(opts = {}) {
|
|
|
3544
4035
|
// src/index.ts
|
|
3545
4036
|
function version() {
|
|
3546
4037
|
try {
|
|
3547
|
-
const pkg = JSON.parse(
|
|
4038
|
+
const pkg = JSON.parse(readFileSync8(join12(dirname6(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
|
|
3548
4039
|
return pkg.version ?? "0.0.0";
|
|
3549
4040
|
} catch {
|
|
3550
4041
|
return "0.0.0";
|
|
3551
4042
|
}
|
|
3552
4043
|
}
|
|
3553
4044
|
function withLock(fn) {
|
|
3554
|
-
const lockPath =
|
|
4045
|
+
const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
3555
4046
|
const lock = acquireLock(lockPath);
|
|
3556
4047
|
if (!lock) {
|
|
3557
4048
|
console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");
|