@swestash/agent-lens 0.2.0 → 0.4.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 +550 -41
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/assets/CopyButton-CDLZMZtV.js +1 -0
- package/web/assets/{Dashboard-BxZigTdE.js → Dashboard-BGVInKFn.js} +1 -1
- package/web/assets/SessionView-C9Nz-Buq.js +12 -0
- package/web/assets/{SkillView-Zs7vZQPU.js → SkillView-z46KtQoA.js} +1 -1
- package/web/assets/{SkillsView-5RaEi7vd.js → SkillsView-BZ4hNSbk.js} +1 -1
- package/web/assets/WorkflowView-eiRtGE5c.js +1 -0
- package/web/assets/{index-753CcEbN.js → index-5gOslh3o.js} +1 -1
- package/web/assets/index-CXU0GJds.css +1 -0
- package/web/assets/{index-CH9ukarY.js → index-Twf3zCly.js} +12 -12
- 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
|
`);
|
|
@@ -1694,6 +1760,8 @@ function flattenText(content) {
|
|
|
1694
1760
|
const text = parts.join("\n").trim();
|
|
1695
1761
|
return text || null;
|
|
1696
1762
|
}
|
|
1763
|
+
var RESULT_SUMMARY_MAX_LINES = 40;
|
|
1764
|
+
var RESULT_SUMMARY_MAX_CHARS = 2e3;
|
|
1697
1765
|
function summarizeResult(content) {
|
|
1698
1766
|
let s = null;
|
|
1699
1767
|
if (typeof content === "string")
|
|
@@ -1711,8 +1779,24 @@ function summarizeResult(content) {
|
|
|
1711
1779
|
}
|
|
1712
1780
|
if (!s)
|
|
1713
1781
|
return null;
|
|
1714
|
-
s = s.replace(/\
|
|
1715
|
-
|
|
1782
|
+
s = s.replace(/\r\n/g, "\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1783
|
+
if (!s)
|
|
1784
|
+
return null;
|
|
1785
|
+
let truncated = false;
|
|
1786
|
+
const lines = s.split("\n");
|
|
1787
|
+
if (lines.length > RESULT_SUMMARY_MAX_LINES) {
|
|
1788
|
+
s = lines.slice(0, RESULT_SUMMARY_MAX_LINES).join("\n");
|
|
1789
|
+
truncated = true;
|
|
1790
|
+
}
|
|
1791
|
+
if (s.length > RESULT_SUMMARY_MAX_CHARS) {
|
|
1792
|
+
s = s.slice(0, RESULT_SUMMARY_MAX_CHARS);
|
|
1793
|
+
const nl = s.lastIndexOf("\n");
|
|
1794
|
+
if (nl > RESULT_SUMMARY_MAX_CHARS - 200)
|
|
1795
|
+
s = s.slice(0, nl);
|
|
1796
|
+
truncated = true;
|
|
1797
|
+
}
|
|
1798
|
+
s = s.trimEnd();
|
|
1799
|
+
return (truncated ? s + "\n\u2026 (truncated)" : s) || null;
|
|
1716
1800
|
}
|
|
1717
1801
|
function asString(v) {
|
|
1718
1802
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
@@ -2608,6 +2692,10 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now, stats) {
|
|
|
2608
2692
|
tx();
|
|
2609
2693
|
}
|
|
2610
2694
|
|
|
2695
|
+
// ../ingest/dist/workflows.js
|
|
2696
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
2697
|
+
import { basename as basename3, join as join5 } from "path";
|
|
2698
|
+
|
|
2611
2699
|
// ../ingest/dist/redact.js
|
|
2612
2700
|
import { createHash as createHash2, randomBytes } from "crypto";
|
|
2613
2701
|
function parseExcludes(csv) {
|
|
@@ -2662,6 +2750,334 @@ function* streamLines(path) {
|
|
|
2662
2750
|
}
|
|
2663
2751
|
}
|
|
2664
2752
|
|
|
2753
|
+
// ../ingest/dist/workflows.js
|
|
2754
|
+
function newWorkflowStats() {
|
|
2755
|
+
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
2756
|
+
}
|
|
2757
|
+
function walkSidecars(dir, inWorkflows, out) {
|
|
2758
|
+
let entries;
|
|
2759
|
+
try {
|
|
2760
|
+
entries = readdirSync3(dir, { withFileTypes: true });
|
|
2761
|
+
} catch {
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
for (const e of entries) {
|
|
2765
|
+
const p = join5(dir, e.name);
|
|
2766
|
+
if (e.isDirectory())
|
|
2767
|
+
walkSidecars(p, inWorkflows || e.name === "workflows", out);
|
|
2768
|
+
else if (e.isFile() && inWorkflows && /^wf_.+\.json$/.test(e.name))
|
|
2769
|
+
out.push(p);
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
function discoverSidecars(sourceArchiveDir) {
|
|
2773
|
+
const out = [];
|
|
2774
|
+
walkSidecars(join5(sourceArchiveDir, "projects"), false, out);
|
|
2775
|
+
try {
|
|
2776
|
+
for (const ts of readdirSync3(join5(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
2777
|
+
if (ts.isDirectory())
|
|
2778
|
+
walkSidecars(join5(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
2779
|
+
} catch {
|
|
2780
|
+
}
|
|
2781
|
+
return out;
|
|
2782
|
+
}
|
|
2783
|
+
function sessionFromPath(path) {
|
|
2784
|
+
const parts = path.split("/");
|
|
2785
|
+
const i = parts.indexOf("workflows");
|
|
2786
|
+
return i > 0 ? parts[i - 1] : null;
|
|
2787
|
+
}
|
|
2788
|
+
function toIso(x) {
|
|
2789
|
+
if (typeof x === "number" && Number.isFinite(x))
|
|
2790
|
+
return new Date(x).toISOString();
|
|
2791
|
+
if (typeof x === "string" && x) {
|
|
2792
|
+
const t = Date.parse(x);
|
|
2793
|
+
return Number.isNaN(t) ? null : new Date(t).toISOString();
|
|
2794
|
+
}
|
|
2795
|
+
return null;
|
|
2796
|
+
}
|
|
2797
|
+
var jsonOrNull = (v) => v == null ? null : JSON.stringify(v);
|
|
2798
|
+
var intOrNull = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
|
|
2799
|
+
var strOrNull = (v) => typeof v === "string" && v ? v : null;
|
|
2800
|
+
function cleanStr(v) {
|
|
2801
|
+
const s = strOrNull(v);
|
|
2802
|
+
if (!s)
|
|
2803
|
+
return null;
|
|
2804
|
+
return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "").replace(/\[[0-9;]+m\]?/g, "").trim() || null;
|
|
2805
|
+
}
|
|
2806
|
+
function ingestWorkflowResults(db, sourceArchiveDir, sourceId, excludedDirs, now, stats, full) {
|
|
2807
|
+
let paths = discoverSidecars(sourceArchiveDir);
|
|
2808
|
+
if (excludedDirs.length)
|
|
2809
|
+
paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
|
|
2810
|
+
paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
|
|
2811
|
+
const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
|
|
2812
|
+
const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
|
|
2813
|
+
VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
|
|
2814
|
+
ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
|
|
2815
|
+
const upsert = db.prepare(`INSERT INTO workflow_results
|
|
2816
|
+
(run_id, source_id, session_id, task_id, workflow_name, status, summary, default_model,
|
|
2817
|
+
result_json, phases_json, logs_json, progress_json,
|
|
2818
|
+
agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at, ingested_at)
|
|
2819
|
+
VALUES
|
|
2820
|
+
(@run_id, @source_id, @session_id, @task_id, @workflow_name, @status, @summary, @default_model,
|
|
2821
|
+
@result_json, @phases_json, @logs_json, @progress_json,
|
|
2822
|
+
@agent_count, @total_tokens, @total_tool_calls, @duration_ms, @started_at, @ended_at, @ingested_at)
|
|
2823
|
+
ON CONFLICT(run_id) DO UPDATE SET
|
|
2824
|
+
source_id=excluded.source_id, session_id=excluded.session_id, task_id=excluded.task_id,
|
|
2825
|
+
workflow_name=excluded.workflow_name, status=excluded.status, summary=excluded.summary,
|
|
2826
|
+
default_model=excluded.default_model, result_json=excluded.result_json, phases_json=excluded.phases_json,
|
|
2827
|
+
logs_json=excluded.logs_json, progress_json=excluded.progress_json, agent_count=excluded.agent_count,
|
|
2828
|
+
total_tokens=excluded.total_tokens, total_tool_calls=excluded.total_tool_calls,
|
|
2829
|
+
duration_ms=excluded.duration_ms, started_at=excluded.started_at, ended_at=excluded.ended_at,
|
|
2830
|
+
ingested_at=excluded.ingested_at`);
|
|
2831
|
+
for (const path of paths) {
|
|
2832
|
+
stats.files++;
|
|
2833
|
+
const st = statSync2(path);
|
|
2834
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2835
|
+
const prev = full ? void 0 : getState.get(path);
|
|
2836
|
+
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
2837
|
+
stats.skipped++;
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
let buf;
|
|
2841
|
+
try {
|
|
2842
|
+
buf = readFileSync4(path);
|
|
2843
|
+
} catch {
|
|
2844
|
+
stats.malformed++;
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
const hash = sha256(buf);
|
|
2848
|
+
if (prev && prev.sha256 === hash) {
|
|
2849
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2850
|
+
stats.skipped++;
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
let d;
|
|
2854
|
+
try {
|
|
2855
|
+
d = JSON.parse(buf.toString("utf8"));
|
|
2856
|
+
} catch {
|
|
2857
|
+
stats.malformed++;
|
|
2858
|
+
continue;
|
|
2859
|
+
}
|
|
2860
|
+
if (!d || typeof d !== "object") {
|
|
2861
|
+
stats.malformed++;
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
const runId = strOrNull(d.runId) ?? basename3(path, ".json");
|
|
2865
|
+
const startedAt = toIso(d.startTime) ?? toIso(d.timestamp);
|
|
2866
|
+
const durationMs2 = intOrNull(d.durationMs);
|
|
2867
|
+
const endedAt = startedAt && durationMs2 != null ? new Date(Date.parse(startedAt) + durationMs2).toISOString() : null;
|
|
2868
|
+
upsert.run({
|
|
2869
|
+
run_id: runId,
|
|
2870
|
+
source_id: sourceId,
|
|
2871
|
+
session_id: sessionFromPath(path),
|
|
2872
|
+
task_id: strOrNull(d.taskId),
|
|
2873
|
+
workflow_name: strOrNull(d.workflowName),
|
|
2874
|
+
status: strOrNull(d.status),
|
|
2875
|
+
summary: strOrNull(d.summary),
|
|
2876
|
+
default_model: cleanStr(d.defaultModel),
|
|
2877
|
+
result_json: jsonOrNull(d.result),
|
|
2878
|
+
phases_json: Array.isArray(d.phases) ? JSON.stringify(d.phases) : null,
|
|
2879
|
+
logs_json: Array.isArray(d.logs) ? JSON.stringify(d.logs) : null,
|
|
2880
|
+
progress_json: Array.isArray(d.workflowProgress) ? JSON.stringify(d.workflowProgress) : null,
|
|
2881
|
+
agent_count: intOrNull(d.agentCount),
|
|
2882
|
+
total_tokens: intOrNull(d.totalTokens),
|
|
2883
|
+
total_tool_calls: intOrNull(d.totalToolCalls),
|
|
2884
|
+
duration_ms: durationMs2,
|
|
2885
|
+
started_at: startedAt,
|
|
2886
|
+
ended_at: endedAt,
|
|
2887
|
+
ingested_at: now
|
|
2888
|
+
});
|
|
2889
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2890
|
+
stats.upserted++;
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
// ../ingest/dist/meta.js
|
|
2895
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
2896
|
+
import { basename as basename4, join as join6 } from "path";
|
|
2897
|
+
function newMetaStats() {
|
|
2898
|
+
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
2899
|
+
}
|
|
2900
|
+
function walkMeta(dir, inSubagents, out) {
|
|
2901
|
+
let entries;
|
|
2902
|
+
try {
|
|
2903
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2904
|
+
} catch {
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
for (const e of entries) {
|
|
2908
|
+
const p = join6(dir, e.name);
|
|
2909
|
+
if (e.isDirectory())
|
|
2910
|
+
walkMeta(p, inSubagents || e.name === "subagents", out);
|
|
2911
|
+
else if (e.isFile() && inSubagents && /^agent-.+\.meta\.json$/.test(e.name))
|
|
2912
|
+
out.push(p);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
function discoverMeta(sourceArchiveDir) {
|
|
2916
|
+
const out = [];
|
|
2917
|
+
walkMeta(join6(sourceArchiveDir, "projects"), false, out);
|
|
2918
|
+
try {
|
|
2919
|
+
for (const ts of readdirSync4(join6(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
2920
|
+
if (ts.isDirectory())
|
|
2921
|
+
walkMeta(join6(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
2922
|
+
} catch {
|
|
2923
|
+
}
|
|
2924
|
+
return out;
|
|
2925
|
+
}
|
|
2926
|
+
var intOrNull2 = (v) => typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : null;
|
|
2927
|
+
var strOrNull2 = (v) => typeof v === "string" && v ? v : null;
|
|
2928
|
+
function ingestSubagentMeta(db, sourceArchiveDir, sourceId, excludedDirs, now, stats, full) {
|
|
2929
|
+
let paths = discoverMeta(sourceArchiveDir);
|
|
2930
|
+
if (excludedDirs.length)
|
|
2931
|
+
paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
|
|
2932
|
+
paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
|
|
2933
|
+
const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
|
|
2934
|
+
const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
|
|
2935
|
+
VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
|
|
2936
|
+
ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
|
|
2937
|
+
const upsert = db.prepare(`INSERT INTO session_meta
|
|
2938
|
+
(session_id, source_id, agent_type, agent_description, spawn_depth, tool_use_id, ingested_at)
|
|
2939
|
+
VALUES
|
|
2940
|
+
(@session_id, @source_id, @agent_type, @agent_description, @spawn_depth, @tool_use_id, @ingested_at)
|
|
2941
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
2942
|
+
source_id=excluded.source_id, agent_type=excluded.agent_type,
|
|
2943
|
+
agent_description=excluded.agent_description, spawn_depth=excluded.spawn_depth,
|
|
2944
|
+
tool_use_id=excluded.tool_use_id, ingested_at=excluded.ingested_at`);
|
|
2945
|
+
for (const path of paths) {
|
|
2946
|
+
stats.files++;
|
|
2947
|
+
const st = statSync3(path);
|
|
2948
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2949
|
+
const prev = full ? void 0 : getState.get(path);
|
|
2950
|
+
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
2951
|
+
stats.skipped++;
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
let buf;
|
|
2955
|
+
try {
|
|
2956
|
+
buf = readFileSync5(path);
|
|
2957
|
+
} catch {
|
|
2958
|
+
stats.malformed++;
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
const hash = sha256(buf);
|
|
2962
|
+
if (prev && prev.sha256 === hash) {
|
|
2963
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2964
|
+
stats.skipped++;
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2967
|
+
let d;
|
|
2968
|
+
try {
|
|
2969
|
+
d = JSON.parse(buf.toString("utf8"));
|
|
2970
|
+
} catch {
|
|
2971
|
+
stats.malformed++;
|
|
2972
|
+
continue;
|
|
2973
|
+
}
|
|
2974
|
+
if (!d || typeof d !== "object") {
|
|
2975
|
+
stats.malformed++;
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
const sessionId = basename4(path, ".meta.json");
|
|
2979
|
+
upsert.run({
|
|
2980
|
+
session_id: sessionId,
|
|
2981
|
+
source_id: sourceId,
|
|
2982
|
+
agent_type: strOrNull2(d.agentType),
|
|
2983
|
+
agent_description: strOrNull2(d.description),
|
|
2984
|
+
spawn_depth: intOrNull2(d.spawnDepth),
|
|
2985
|
+
tool_use_id: strOrNull2(d.toolUseId),
|
|
2986
|
+
ingested_at: now
|
|
2987
|
+
});
|
|
2988
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
2989
|
+
stats.upserted++;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
|
|
2993
|
+
// ../ingest/dist/toolResults.js
|
|
2994
|
+
import { readdirSync as readdirSync5, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
2995
|
+
import { basename as basename5, join as join7 } from "path";
|
|
2996
|
+
function newToolResultStats() {
|
|
2997
|
+
return { files: 0, upserted: 0, skipped: 0, malformed: 0 };
|
|
2998
|
+
}
|
|
2999
|
+
function walkResults(dir, inResults, out) {
|
|
3000
|
+
let entries;
|
|
3001
|
+
try {
|
|
3002
|
+
entries = readdirSync5(dir, { withFileTypes: true });
|
|
3003
|
+
} catch {
|
|
3004
|
+
return;
|
|
3005
|
+
}
|
|
3006
|
+
for (const e of entries) {
|
|
3007
|
+
const p = join7(dir, e.name);
|
|
3008
|
+
if (e.isDirectory())
|
|
3009
|
+
walkResults(p, inResults || e.name === "tool-results", out);
|
|
3010
|
+
else if (e.isFile() && inResults && e.name.endsWith(".txt"))
|
|
3011
|
+
out.push(p);
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
function discoverResults(sourceArchiveDir) {
|
|
3015
|
+
const out = [];
|
|
3016
|
+
walkResults(join7(sourceArchiveDir, "projects"), false, out);
|
|
3017
|
+
try {
|
|
3018
|
+
for (const ts of readdirSync5(join7(sourceArchiveDir, ".versions"), { withFileTypes: true }))
|
|
3019
|
+
if (ts.isDirectory())
|
|
3020
|
+
walkResults(join7(sourceArchiveDir, ts.name, "projects"), false, out);
|
|
3021
|
+
} catch {
|
|
3022
|
+
}
|
|
3023
|
+
return out;
|
|
3024
|
+
}
|
|
3025
|
+
function sessionFromPath2(path) {
|
|
3026
|
+
const parts = path.split("/");
|
|
3027
|
+
const i = parts.indexOf("tool-results");
|
|
3028
|
+
return i > 0 ? parts[i - 1] : null;
|
|
3029
|
+
}
|
|
3030
|
+
function ingestToolResults(db, sourceArchiveDir, excludedDirs, now, stats, full) {
|
|
3031
|
+
let paths = discoverResults(sourceArchiveDir);
|
|
3032
|
+
if (excludedDirs.length)
|
|
3033
|
+
paths = paths.filter((p) => !isExcludedArchivePath(p, excludedDirs));
|
|
3034
|
+
paths.sort((a, b) => Number(a.includes("/.versions/")) - Number(b.includes("/.versions/")) || (a < b ? -1 : 1));
|
|
3035
|
+
const getState = db.prepare("SELECT size, mtime_ms, sha256 FROM ingest_state WHERE file_path = ?");
|
|
3036
|
+
const setState = db.prepare(`INSERT INTO ingest_state (file_path, size, mtime_ms, sha256, events_ingested, ingested_at)
|
|
3037
|
+
VALUES (@file_path, @size, @mtime_ms, @sha256, 0, @ingested_at)
|
|
3038
|
+
ON CONFLICT(file_path) DO UPDATE SET size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256, ingested_at=excluded.ingested_at`);
|
|
3039
|
+
const upsert = db.prepare(`INSERT INTO tool_results (session_id, name, path, bytes, text, ingested_at)
|
|
3040
|
+
VALUES (@session_id, @name, @path, @bytes, @text, @ingested_at)
|
|
3041
|
+
ON CONFLICT(session_id, name) DO UPDATE SET
|
|
3042
|
+
path=excluded.path, bytes=excluded.bytes, text=excluded.text, ingested_at=excluded.ingested_at`);
|
|
3043
|
+
for (const path of paths) {
|
|
3044
|
+
const sessionId = sessionFromPath2(path);
|
|
3045
|
+
if (!sessionId)
|
|
3046
|
+
continue;
|
|
3047
|
+
stats.files++;
|
|
3048
|
+
const st = statSync4(path);
|
|
3049
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
3050
|
+
const prev = full ? void 0 : getState.get(path);
|
|
3051
|
+
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
3052
|
+
stats.skipped++;
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
let buf;
|
|
3056
|
+
try {
|
|
3057
|
+
buf = readFileSync6(path);
|
|
3058
|
+
} catch {
|
|
3059
|
+
stats.malformed++;
|
|
3060
|
+
continue;
|
|
3061
|
+
}
|
|
3062
|
+
const hash = sha256(buf);
|
|
3063
|
+
if (prev && prev.sha256 === hash) {
|
|
3064
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
3065
|
+
stats.skipped++;
|
|
3066
|
+
continue;
|
|
3067
|
+
}
|
|
3068
|
+
upsert.run({
|
|
3069
|
+
session_id: sessionId,
|
|
3070
|
+
name: basename5(path, ".txt"),
|
|
3071
|
+
path,
|
|
3072
|
+
bytes: st.size,
|
|
3073
|
+
text: buf.toString("utf8"),
|
|
3074
|
+
ingested_at: now
|
|
3075
|
+
});
|
|
3076
|
+
setState.run({ file_path: path, size: st.size, mtime_ms: mtimeMs, sha256: hash, ingested_at: now });
|
|
3077
|
+
stats.upserted++;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
|
|
2665
3081
|
// ../ingest/dist/run.js
|
|
2666
3082
|
function parseIngestArgs(argv) {
|
|
2667
3083
|
const a = { full: false, db: "", archive: "" };
|
|
@@ -2678,16 +3094,24 @@ function parseIngestArgs(argv) {
|
|
|
2678
3094
|
function runIngest(argv = process.argv.slice(2)) {
|
|
2679
3095
|
const args = parseIngestArgs(argv);
|
|
2680
3096
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
2681
|
-
const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE ||
|
|
2682
|
-
const dbPath = args.db || process.env.AGENT_LENS_DB ||
|
|
3097
|
+
const archiveRoot = args.archive || process.env.AGENT_LENS_ARCHIVE || join8(dataDir, "archive");
|
|
3098
|
+
const dbPath = args.db || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
|
|
2683
3099
|
if (!existsSync4(archiveRoot)) {
|
|
2684
3100
|
console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
|
|
2685
3101
|
process.exit(1);
|
|
2686
3102
|
}
|
|
2687
3103
|
mkdirSync4(dirname4(dbPath), { recursive: true });
|
|
2688
3104
|
const db = args.full ? openRaw(dbPath) : openDb(dbPath);
|
|
2689
|
-
if (args.full)
|
|
3105
|
+
if (args.full) {
|
|
2690
3106
|
resetSchema(db);
|
|
3107
|
+
} else {
|
|
3108
|
+
const dbVer = readSchemaVersion(db);
|
|
3109
|
+
if (dbVer != null && dbVer !== SCHEMA_VERSION) {
|
|
3110
|
+
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.`);
|
|
3111
|
+
db.close();
|
|
3112
|
+
process.exit(1);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
2691
3115
|
const adapterList = [new ClaudeCodeAdapter()];
|
|
2692
3116
|
const adapterById = new Map(adapterList.map((a) => [a.agentId, a]));
|
|
2693
3117
|
const sources = loadSources();
|
|
@@ -2697,6 +3121,9 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2697
3121
|
const pruned = pruneExcluded(db, excludePaths);
|
|
2698
3122
|
const stmts = prepareStatements(db);
|
|
2699
3123
|
const stats = newStats();
|
|
3124
|
+
const wfStats = newWorkflowStats();
|
|
3125
|
+
const metaStats = newMetaStats();
|
|
3126
|
+
const trStats = newToolResultStats();
|
|
2700
3127
|
const dirty = /* @__PURE__ */ new Set();
|
|
2701
3128
|
for (const source of sources) {
|
|
2702
3129
|
const adapter = adapterById.get(source.agent);
|
|
@@ -2706,13 +3133,13 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2706
3133
|
}
|
|
2707
3134
|
stmts.insAgent.run(adapter.agentId, adapter.agentName);
|
|
2708
3135
|
stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
|
|
2709
|
-
let files = adapter.discover(
|
|
3136
|
+
let files = adapter.discover(join8(archiveRoot, source.label), source.label);
|
|
2710
3137
|
if (excludedDirs.length)
|
|
2711
3138
|
files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
|
|
2712
3139
|
files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
|
|
2713
3140
|
for (const file of files) {
|
|
2714
3141
|
stats.files++;
|
|
2715
|
-
const st =
|
|
3142
|
+
const st = statSync5(file.path);
|
|
2716
3143
|
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
2717
3144
|
const prev = args.full ? void 0 : stmts.getState.get(file.path);
|
|
2718
3145
|
if (prev && prev.size === st.size && prev.mtime_ms === mtimeMs) {
|
|
@@ -2720,7 +3147,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2720
3147
|
continue;
|
|
2721
3148
|
}
|
|
2722
3149
|
const small = st.size <= STREAM_THRESHOLD;
|
|
2723
|
-
const buf = small ?
|
|
3150
|
+
const buf = small ? readFileSync7(file.path) : null;
|
|
2724
3151
|
const hash = small ? sha256(buf) : sha256File(file.path);
|
|
2725
3152
|
if (prev && prev.sha256 === hash) {
|
|
2726
3153
|
stmts.setState.run({
|
|
@@ -2738,6 +3165,9 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2738
3165
|
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash }, now, stats);
|
|
2739
3166
|
dirty.add(file.sessionId);
|
|
2740
3167
|
}
|
|
3168
|
+
ingestWorkflowResults(db, join8(archiveRoot, source.label), source.label, excludedDirs, now, wfStats, args.full);
|
|
3169
|
+
ingestSubagentMeta(db, join8(archiveRoot, source.label), source.label, excludedDirs, now, metaStats, args.full);
|
|
3170
|
+
ingestToolResults(db, join8(archiveRoot, source.label), excludedDirs, now, trStats, args.full);
|
|
2741
3171
|
}
|
|
2742
3172
|
const expanded = rebuildDerived(db, args.full ? null : dirty);
|
|
2743
3173
|
const classified = classify(db, args.full ? null : expanded);
|
|
@@ -2753,9 +3183,15 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
2753
3183
|
cost += costForUsage(u.model, u);
|
|
2754
3184
|
totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
|
|
2755
3185
|
}
|
|
3186
|
+
const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
|
|
3187
|
+
const metaRows = count("SELECT COUNT(*) n FROM session_meta");
|
|
3188
|
+
const trRows = count("SELECT COUNT(*) n FROM tool_results");
|
|
2756
3189
|
db.close();
|
|
2757
3190
|
console.log(`agent-lens-ingest: files=${stats.files} skipped=${stats.skipped} new_events=${stats.newEvents} malformed=${stats.malformed}${pruned ? ` excluded_pruned=${pruned}` : ""}
|
|
2758
3191
|
sessions=${sessions} turns=${turns} events=${events} tool_calls=${tools} classified=${classified.count}
|
|
3192
|
+
workflow_results=${wfRuns} (sidecar upserted=${wfStats.upserted} skipped=${wfStats.skipped} malformed=${wfStats.malformed})
|
|
3193
|
+
session_meta=${metaRows} (upserted=${metaStats.upserted} skipped=${metaStats.skipped} malformed=${metaStats.malformed})
|
|
3194
|
+
tool_results=${trRows} (upserted=${trStats.upserted} skipped=${trStats.skipped} malformed=${trStats.malformed})
|
|
2759
3195
|
tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
|
|
2760
3196
|
}
|
|
2761
3197
|
function runMetrics(argv = process.argv.slice(2)) {
|
|
@@ -2764,7 +3200,7 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
2764
3200
|
if (argv[i] === "--db")
|
|
2765
3201
|
dbArg = argv[++i] ?? "";
|
|
2766
3202
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
2767
|
-
const dbPath = dbArg || process.env.AGENT_LENS_DB ||
|
|
3203
|
+
const dbPath = dbArg || process.env.AGENT_LENS_DB || join8(dataDir, "agent-lens.db");
|
|
2768
3204
|
if (!existsSync4(dbPath)) {
|
|
2769
3205
|
console.error(`agent-lens-metrics: db not found: ${dbPath} (run ingest first)`);
|
|
2770
3206
|
process.exit(1);
|
|
@@ -2777,7 +3213,7 @@ function runMetrics(argv = process.argv.slice(2)) {
|
|
|
2777
3213
|
|
|
2778
3214
|
// ../server/dist/run.js
|
|
2779
3215
|
import { existsSync as existsSync7 } from "fs";
|
|
2780
|
-
import { dirname as dirname5, join as
|
|
3216
|
+
import { dirname as dirname5, join as join10 } from "path";
|
|
2781
3217
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2782
3218
|
|
|
2783
3219
|
// ../server/dist/db.js
|
|
@@ -2787,6 +3223,12 @@ function openReadonly(file) {
|
|
|
2787
3223
|
db.pragma("query_only = ON");
|
|
2788
3224
|
return db;
|
|
2789
3225
|
}
|
|
3226
|
+
function schemaStatus(db) {
|
|
3227
|
+
const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
|
|
3228
|
+
const dbVersion = row ? Number(row.value) : null;
|
|
3229
|
+
const db_version = dbVersion != null && Number.isFinite(dbVersion) ? dbVersion : null;
|
|
3230
|
+
return { db_version, expected: SCHEMA_VERSION, stale: db_version != null && db_version !== SCHEMA_VERSION };
|
|
3231
|
+
}
|
|
2790
3232
|
function lastIngested(db) {
|
|
2791
3233
|
const row = db.prepare("SELECT MAX(ingested_at) AS last FROM ingest_state").get();
|
|
2792
3234
|
return row?.last ?? null;
|
|
@@ -2937,6 +3379,17 @@ function getSession(db, id) {
|
|
|
2937
3379
|
total_duration_ms, total_tokens, input_json, result_summary,
|
|
2938
3380
|
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tool_calls.workflow_run_id) AS workflow_agent_count
|
|
2939
3381
|
FROM tool_calls WHERE session_id = ?`).all(id);
|
|
3382
|
+
if (tableExists(db, "tool_results")) {
|
|
3383
|
+
const getFull = db.prepare("SELECT text, bytes FROM tool_results WHERE session_id = ? AND name = ?");
|
|
3384
|
+
for (const t of toolRows) {
|
|
3385
|
+
const m = typeof t.result_summary === "string" ? t.result_summary.match(/tool-results\/([A-Za-z0-9_-]+)\.txt/) : null;
|
|
3386
|
+
if (!m)
|
|
3387
|
+
continue;
|
|
3388
|
+
const full = getFull.get(id, m[1]);
|
|
3389
|
+
if (full)
|
|
3390
|
+
t.full_result = { text: full.text, bytes: full.bytes };
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
2940
3393
|
const toolsByEvent = /* @__PURE__ */ new Map();
|
|
2941
3394
|
for (const t of toolRows) {
|
|
2942
3395
|
if (!t.event_uuid)
|
|
@@ -3004,9 +3457,12 @@ function getSession(db, id) {
|
|
|
3004
3457
|
if (p)
|
|
3005
3458
|
parent = { id: p.id, title: p.ai_title || p.slug || null, turn_seq: p.turn_seq ?? null };
|
|
3006
3459
|
}
|
|
3460
|
+
const hasMeta = tableExists(db, "session_meta");
|
|
3007
3461
|
const children = db.prepare(`SELECT s.id, s.ai_title, s.slug, s.turn_count, s.started_at, s.workflow_run_id,
|
|
3008
3462
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
3009
|
-
|
|
3463
|
+
${hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth"}
|
|
3464
|
+
FROM sessions s ${hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : ""}
|
|
3465
|
+
WHERE s.parent_session_id = ? ORDER BY s.started_at`).all(id);
|
|
3010
3466
|
for (const ch of children) {
|
|
3011
3467
|
ch.title = ch.ai_title || ch.slug || null;
|
|
3012
3468
|
const u = db.prepare(`SELECT model, SUM(input_tokens) i, SUM(output_tokens) o,
|
|
@@ -3015,8 +3471,10 @@ function getSession(db, id) {
|
|
|
3015
3471
|
ch.tokens = u.reduce((a, r) => a + r.i + r.o + r.cw + r.cr, 0);
|
|
3016
3472
|
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
3473
|
}
|
|
3474
|
+
const hasWR = tableExists(db, "workflow_results");
|
|
3018
3475
|
const workflow_runs = db.prepare(`SELECT tc.workflow_run_id AS run_id, tc.workflow_name AS name, t.seq AS turn_seq,
|
|
3019
3476
|
(SELECT COUNT(*) FROM sessions s WHERE s.workflow_run_id = tc.workflow_run_id) AS agent_count
|
|
3477
|
+
${hasWR ? ", (SELECT status FROM workflow_results wr WHERE wr.run_id = tc.workflow_run_id) AS status" : ", NULL AS status"}
|
|
3020
3478
|
FROM tool_calls tc LEFT JOIN turns t ON t.id = tc.turn_id
|
|
3021
3479
|
WHERE tc.session_id = ? AND tc.workflow_run_id IS NOT NULL
|
|
3022
3480
|
ORDER BY t.seq`).all(id);
|
|
@@ -3025,8 +3483,21 @@ function getSession(db, id) {
|
|
|
3025
3483
|
function xmlTag(text, tag) {
|
|
3026
3484
|
return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
|
|
3027
3485
|
}
|
|
3486
|
+
function tableExists(db, name) {
|
|
3487
|
+
return !!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name = ?").get(name);
|
|
3488
|
+
}
|
|
3489
|
+
function safeJson(s) {
|
|
3490
|
+
if (!s)
|
|
3491
|
+
return null;
|
|
3492
|
+
try {
|
|
3493
|
+
return JSON.parse(s);
|
|
3494
|
+
} catch {
|
|
3495
|
+
return null;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3028
3498
|
function getWorkflow(db, runId) {
|
|
3029
3499
|
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,
|
|
3500
|
+
tc.input_json AS input_json,
|
|
3030
3501
|
tc.session_id AS parent_session_id, t.seq AS turn_seq,
|
|
3031
3502
|
ps.ai_title AS parent_ai_title, ps.slug AS parent_slug
|
|
3032
3503
|
FROM tool_calls tc
|
|
@@ -3037,9 +3508,12 @@ function getWorkflow(db, runId) {
|
|
|
3037
3508
|
LIMIT 1`).get(runId);
|
|
3038
3509
|
if (!wf)
|
|
3039
3510
|
return null;
|
|
3511
|
+
const hasMeta = tableExists(db, "session_meta");
|
|
3040
3512
|
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
3513
|
(SELECT GROUP_CONCAT(DISTINCT model) FROM token_usage t WHERE t.session_id = s.id) AS models
|
|
3042
|
-
|
|
3514
|
+
${hasMeta ? ", sm.agent_type, sm.agent_description, sm.spawn_depth" : ", NULL AS agent_type, NULL AS agent_description, NULL AS spawn_depth"}
|
|
3515
|
+
FROM sessions s ${hasMeta ? "LEFT JOIN session_meta sm ON sm.session_id = s.id" : ""}
|
|
3516
|
+
WHERE s.workflow_run_id = ? ORDER BY s.started_at`).all(runId);
|
|
3043
3517
|
let total_tokens = 0;
|
|
3044
3518
|
let total_cost = 0;
|
|
3045
3519
|
let started_at = null;
|
|
@@ -3070,12 +3544,44 @@ function getWorkflow(db, runId) {
|
|
|
3070
3544
|
completion = { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
|
|
3071
3545
|
}
|
|
3072
3546
|
}
|
|
3547
|
+
const wr = tableExists(db, "workflow_results") ? db.prepare(`SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,
|
|
3548
|
+
agent_count, total_tokens, total_tool_calls, duration_ms, started_at, ended_at
|
|
3549
|
+
FROM workflow_results WHERE run_id = ?`).get(wf.run_id) : null;
|
|
3550
|
+
let run = null;
|
|
3551
|
+
if (wr) {
|
|
3552
|
+
run = {
|
|
3553
|
+
status: wr.status ?? null,
|
|
3554
|
+
summary: wr.summary ?? null,
|
|
3555
|
+
default_model: wr.default_model ?? null,
|
|
3556
|
+
agent_count: wr.agent_count ?? null,
|
|
3557
|
+
total_tokens: wr.total_tokens ?? null,
|
|
3558
|
+
total_tool_calls: wr.total_tool_calls ?? null,
|
|
3559
|
+
duration_ms: wr.duration_ms ?? null,
|
|
3560
|
+
started_at: wr.started_at ?? null,
|
|
3561
|
+
ended_at: wr.ended_at ?? null,
|
|
3562
|
+
phases: safeJson(wr.phases_json),
|
|
3563
|
+
logs: safeJson(wr.logs_json),
|
|
3564
|
+
// The per-phase/per-agent event timeline (workflowProgress) — lets the page render a phase graph
|
|
3565
|
+
// with a per-phase descriptor (agent count, models). Absent on older/failed runs → UI falls back.
|
|
3566
|
+
progress: safeJson(wr.progress_json)
|
|
3567
|
+
};
|
|
3568
|
+
if (wr.result_json != null || wr.status) {
|
|
3569
|
+
completion = {
|
|
3570
|
+
status: wr.status ?? completion?.status ?? null,
|
|
3571
|
+
summary: wr.summary ?? completion?.summary ?? null,
|
|
3572
|
+
result: wr.result_json ?? completion?.result ?? null,
|
|
3573
|
+
failures: completion?.failures ?? null
|
|
3574
|
+
};
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3073
3577
|
return {
|
|
3074
3578
|
run_id: wf.run_id,
|
|
3075
3579
|
name: wf.name ?? null,
|
|
3076
|
-
status: wf.status ?? null,
|
|
3580
|
+
status: wr?.status ?? wf.status ?? null,
|
|
3077
3581
|
result_summary: wf.result_summary ?? null,
|
|
3582
|
+
input_json: wf.input_json ?? null,
|
|
3078
3583
|
completion,
|
|
3584
|
+
run,
|
|
3079
3585
|
parent: { id: wf.parent_session_id, title: wf.parent_ai_title || wf.parent_slug || null, turn_seq: wf.turn_seq ?? null },
|
|
3080
3586
|
agents,
|
|
3081
3587
|
stats: { agent_count: agents.length, total_tokens, total_cost: Number(total_cost.toFixed(4)), started_at, ended_at, duration_ms }
|
|
@@ -3305,7 +3811,7 @@ function dashboardBreakdowns(db, f) {
|
|
|
3305
3811
|
|
|
3306
3812
|
// ../server/dist/refresh.js
|
|
3307
3813
|
import { existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
|
|
3308
|
-
import { join as
|
|
3814
|
+
import { join as join9 } from "path";
|
|
3309
3815
|
function originAllowed(origin) {
|
|
3310
3816
|
if (!origin)
|
|
3311
3817
|
return true;
|
|
@@ -3318,12 +3824,12 @@ function originAllowed(origin) {
|
|
|
3318
3824
|
}
|
|
3319
3825
|
function runRefresh() {
|
|
3320
3826
|
const dataDir = resolveDataDir(findRepoRoot());
|
|
3321
|
-
const lock = acquireLock(
|
|
3827
|
+
const lock = acquireLock(join9(dataDir, ".agent-lens.lock"));
|
|
3322
3828
|
if (!lock)
|
|
3323
3829
|
return null;
|
|
3324
3830
|
try {
|
|
3325
3831
|
const collected = collectAll();
|
|
3326
|
-
const archiveRoot = process.env.AGENT_LENS_ARCHIVE ||
|
|
3832
|
+
const archiveRoot = process.env.AGENT_LENS_ARCHIVE || join9(dataDir, "archive");
|
|
3327
3833
|
if (!existsSync5(archiveRoot))
|
|
3328
3834
|
mkdirSync5(archiveRoot, { recursive: true });
|
|
3329
3835
|
runIngest([]);
|
|
@@ -3336,7 +3842,10 @@ function runRefresh() {
|
|
|
3336
3842
|
// ../server/dist/app.js
|
|
3337
3843
|
async function createApp(db, opts = {}) {
|
|
3338
3844
|
const app = Fastify({ logger: false });
|
|
3339
|
-
app.get("/api/health", async () =>
|
|
3845
|
+
app.get("/api/health", async () => {
|
|
3846
|
+
const s = schemaStatus(db);
|
|
3847
|
+
return { ok: true, last_ingested: lastIngested(db), schema_version: s.db_version, schema_stale: s.stale };
|
|
3848
|
+
});
|
|
3340
3849
|
app.post("/api/refresh", async (req, reply) => {
|
|
3341
3850
|
if (!originAllowed(req.headers.origin)) {
|
|
3342
3851
|
return reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "cross-origin refresh blocked" } });
|
|
@@ -3452,7 +3961,7 @@ async function createApp(db, opts = {}) {
|
|
|
3452
3961
|
async function startServer() {
|
|
3453
3962
|
const here = dirname5(fileURLToPath2(import.meta.url));
|
|
3454
3963
|
const repoRoot = findRepoRoot(here);
|
|
3455
|
-
const dbPath = process.env.AGENT_LENS_DB ||
|
|
3964
|
+
const dbPath = process.env.AGENT_LENS_DB || join10(resolveDataDir(repoRoot), "agent-lens.db");
|
|
3456
3965
|
const host = process.env.AGENT_LENS_HOST || "127.0.0.1";
|
|
3457
3966
|
const port = Number(process.env.AGENT_LENS_PORT || 4477);
|
|
3458
3967
|
const webDist = resolveWebDist(here, repoRoot);
|
|
@@ -3479,11 +3988,11 @@ async function startServer() {
|
|
|
3479
3988
|
|
|
3480
3989
|
// src/watch.ts
|
|
3481
3990
|
import { existsSync as existsSync8 } from "fs";
|
|
3482
|
-
import { join as
|
|
3991
|
+
import { join as join11 } from "path";
|
|
3483
3992
|
import chokidar from "chokidar";
|
|
3484
3993
|
async function runWatch(opts = {}) {
|
|
3485
3994
|
const sources = loadSources();
|
|
3486
|
-
const lockPath =
|
|
3995
|
+
const lockPath = join11(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
3487
3996
|
let running = false;
|
|
3488
3997
|
let pending = false;
|
|
3489
3998
|
const cycle = () => {
|
|
@@ -3516,7 +4025,7 @@ async function runWatch(opts = {}) {
|
|
|
3516
4025
|
}
|
|
3517
4026
|
};
|
|
3518
4027
|
cycle();
|
|
3519
|
-
const watchPaths = sources.map((s) =>
|
|
4028
|
+
const watchPaths = sources.map((s) => join11(s.configDir, "projects")).filter((p) => existsSync8(p));
|
|
3520
4029
|
if (watchPaths.length === 0) {
|
|
3521
4030
|
console.error("agent-lens watch: no source 'projects' directories found \u2014 nothing to watch");
|
|
3522
4031
|
}
|
|
@@ -3544,14 +4053,14 @@ async function runWatch(opts = {}) {
|
|
|
3544
4053
|
// src/index.ts
|
|
3545
4054
|
function version() {
|
|
3546
4055
|
try {
|
|
3547
|
-
const pkg = JSON.parse(
|
|
4056
|
+
const pkg = JSON.parse(readFileSync8(join12(dirname6(fileURLToPath3(import.meta.url)), "../package.json"), "utf8"));
|
|
3548
4057
|
return pkg.version ?? "0.0.0";
|
|
3549
4058
|
} catch {
|
|
3550
4059
|
return "0.0.0";
|
|
3551
4060
|
}
|
|
3552
4061
|
}
|
|
3553
4062
|
function withLock(fn) {
|
|
3554
|
-
const lockPath =
|
|
4063
|
+
const lockPath = join12(resolveDataDir(findRepoRoot()), ".agent-lens.lock");
|
|
3555
4064
|
const lock = acquireLock(lockPath);
|
|
3556
4065
|
if (!lock) {
|
|
3557
4066
|
console.error("agent-lens: another collect/ingest run is in progress \u2014 skipping");
|