@swestash/agent-lens 0.17.0 → 0.17.2
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 +361 -111
- package/dist/agent-lens.js.map +1 -1
- package/package.json +1 -1
- package/web/404.html +2 -2
- package/web/assets/{AboutView-BiuYVPOe.js → AboutView-cNKRITEW.js} +1 -1
- package/web/assets/{Dashboard-fPsqJm-5.js → Dashboard-AjpnMF4y.js} +1 -1
- package/web/assets/{FileView-CX-Pe5S0.js → FileView-dsp2ReoN.js} +1 -1
- package/web/assets/{FilesView-CPaEQFkv.js → FilesView-CedaWAxS.js} +1 -1
- package/web/assets/{SecurityView-D3qd2-q5.js → SecurityView-DDBov1tl.js} +1 -1
- package/web/assets/{SessionView-DtykJhVv.js → SessionView-Y7_R_3iF.js} +1 -1
- package/web/assets/{SkillView-D72WQ-Xv.js → SkillView-CHYZYZwQ.js} +1 -1
- package/web/assets/{SkillsView-CW8sNMN7.js → SkillsView-DWMs97v0.js} +1 -1
- package/web/assets/{WorkflowView-Dek4WLJa.js → WorkflowView-BThiSvTY.js} +1 -1
- package/web/assets/{index-DLoMRgQ8.js → index-CLk0qZiT.js} +1 -1
- package/web/assets/{index-CG2N8ixY.js → index-CSqgNF0A.js} +2 -2
- package/web/assets/index-FnqCa_GV.css +1 -0
- package/web/assets/{jsonish-B0CzuJ3V.js → jsonish-DTF7FYHm.js} +1 -1
- package/web/assets/{theme-DiAReGS9.js → theme-BvznOeGO.js} +1 -1
- package/web/assets/{useResetOn-CoUOqjZU.js → useResetOn-CnN8pwt1.js} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-DN9qB28e.css +0 -1
package/dist/agent-lens.js
CHANGED
|
@@ -592,6 +592,11 @@ var cac = (name = "") => new CAC(name);
|
|
|
592
592
|
|
|
593
593
|
// ../core/dist/schema.js
|
|
594
594
|
var SCHEMA_VERSION = 15;
|
|
595
|
+
var ANALYZE_SQL = "PRAGMA analysis_limit = 1000; ANALYZE;";
|
|
596
|
+
var BULK_DEFERRED = {
|
|
597
|
+
indexes: ["idx_events_type", "idx_events_ts", "idx_events_turn_reply", "idx_events_uuid_cover"],
|
|
598
|
+
triggers: ["events_ai", "events_ad", "events_au"]
|
|
599
|
+
};
|
|
595
600
|
var SCHEMA_SQL = (
|
|
596
601
|
/* sql */
|
|
597
602
|
`
|
|
@@ -694,9 +699,22 @@ CREATE TABLE IF NOT EXISTS events (
|
|
|
694
699
|
source_file TEXT -- archive path the canonical copy came from
|
|
695
700
|
);
|
|
696
701
|
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
|
|
697
|
-
CREATE INDEX IF NOT EXISTS idx_events_turn ON events(turn_id);
|
|
698
702
|
CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
|
|
699
703
|
CREATE INDEX IF NOT EXISTS idx_events_ts ON events(timestamp);
|
|
704
|
+
-- Covering indexes for the dashboard joins. An events row averages ~3.4 KB (raw_json + text), so
|
|
705
|
+
-- about one row fits per page, and a join that reads the row instead of an index pays one random
|
|
706
|
+
-- page read per event: on the real corpus /dashboard/time took 31s from a cold cache. These hold
|
|
707
|
+
-- exactly the columns those joins read. The PK lookups (token_usage, tool_calls, findings,
|
|
708
|
+
-- file_changes \u2192 events by uuid) want timestamp and model, and model latency wants the first
|
|
709
|
+
-- assistant timestamp per turn. Both are index-only scans now, and the guard in
|
|
710
|
+
-- packages/server/test/query-plans.test.ts keeps them that way.
|
|
711
|
+
-- No SCHEMA_VERSION bump: indexes need no rebuild, and CREATE IF NOT EXISTS adds them on the next
|
|
712
|
+
-- incremental ingest. A bump would make that ingest refuse and demand --full.
|
|
713
|
+
-- An index's columns cannot be widened in place, so idx_events_turn is replaced under a new name.
|
|
714
|
+
-- idx_events_turn_reply serves every lookup the old one did, because turn_id is still its prefix.
|
|
715
|
+
DROP INDEX IF EXISTS idx_events_turn;
|
|
716
|
+
CREATE INDEX IF NOT EXISTS idx_events_turn_reply ON events(turn_id, role, timestamp);
|
|
717
|
+
CREATE INDEX IF NOT EXISTS idx_events_uuid_cover ON events(uuid, timestamp, model);
|
|
700
718
|
|
|
701
719
|
-- Tool invocations (incl. subagents / Task calls). -------------------------
|
|
702
720
|
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
@@ -721,13 +739,32 @@ CREATE TABLE IF NOT EXISTS tool_calls (
|
|
|
721
739
|
input_json TEXT, -- tool input, verbatim
|
|
722
740
|
result_summary TEXT -- short result projection (no secrets dumped wholesale)
|
|
723
741
|
);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
742
|
+
-- Covering indexes for the dashboard aggregates. A tool_calls row averages ~2.3 KB (input_json), and
|
|
743
|
+
-- the aggregates read only a handful of narrow columns. Each index below carries all of them, so
|
|
744
|
+
-- whether the planner drives from sessions (a selective source/date filter) or scans tool_calls
|
|
745
|
+
-- (no filter), it never reads the row. Before this, the errored-calls series scanned the whole
|
|
746
|
+
-- table, 7.5s cold on the real corpus. packages/server/test/query-plans.test.ts holds the line.
|
|
747
|
+
-- They replace the single-column session/tool/skill/skill_id/error_type indexes under new names, because
|
|
748
|
+
-- CREATE IF NOT EXISTS cannot widen an index that already exists. Each new index begins with the old
|
|
749
|
+
-- index's column, so every existing lookup still uses it. Same no-bump reasoning as the events
|
|
750
|
+
-- indexes above.
|
|
751
|
+
DROP INDEX IF EXISTS idx_tool_calls_session;
|
|
752
|
+
DROP INDEX IF EXISTS idx_tool_calls_tool;
|
|
753
|
+
DROP INDEX IF EXISTS idx_tool_calls_skill;
|
|
754
|
+
DROP INDEX IF EXISTS idx_tool_calls_skill_id;
|
|
755
|
+
DROP INDEX IF EXISTS idx_tool_calls_error_type;
|
|
756
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_by_session
|
|
757
|
+
ON tool_calls(session_id, tool_name, status, error_type, skill_name, skill_id, agent_type, event_uuid);
|
|
758
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_by_tool ON tool_calls(tool_name, session_id, status, error_type, event_uuid);
|
|
759
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_by_skill ON tool_calls(skill_name, skill_id, session_id, event_uuid);
|
|
760
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_by_skill_id ON tool_calls(skill_id, skill_name, session_id, event_uuid);
|
|
761
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_by_error ON tool_calls(error_type, status, session_id, event_uuid);
|
|
728
762
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_spawned ON tool_calls(spawned_session_id);
|
|
729
763
|
CREATE INDEX IF NOT EXISTS idx_tool_calls_workflow_run ON tool_calls(workflow_run_id);
|
|
730
|
-
|
|
764
|
+
-- Every column that REFERENCES turns(id) needs an index: deleting a turn makes SQLite look up its
|
|
765
|
+
-- referencing rows, and without one that is a full scan of this table per deleted turn. The
|
|
766
|
+
-- incremental rebuild deletes the dirty sessions' turns, so five changed files cost 12.9 GB of reads.
|
|
767
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_turn ON tool_calls(turn_id);
|
|
731
768
|
|
|
732
769
|
-- Skill *versions* (content-addressed). A skill firing injects the full SKILL.md body into the
|
|
733
770
|
-- transcript as an isMeta user event (begins "Base directory for this skill: \u2026", then the body,
|
|
@@ -767,6 +804,8 @@ CREATE TABLE IF NOT EXISTS token_usage (
|
|
|
767
804
|
service_tier TEXT
|
|
768
805
|
);
|
|
769
806
|
CREATE INDEX IF NOT EXISTS idx_token_usage_session ON token_usage(session_id);
|
|
807
|
+
-- Foreign-key lookup for deleting turns; see idx_tool_calls_turn.
|
|
808
|
+
CREATE INDEX IF NOT EXISTS idx_token_usage_turn ON token_usage(turn_id);
|
|
770
809
|
-- Partial so rare usage rows lacking a message_id (e.g. <synthetic>) don't collide on NULL. --
|
|
771
810
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_token_usage_msg
|
|
772
811
|
ON token_usage(session_id, message_id) WHERE message_id IS NOT NULL;
|
|
@@ -2282,9 +2321,11 @@ import { dirname as dirname6, join as join9 } from "node:path";
|
|
|
2282
2321
|
|
|
2283
2322
|
// ../ingest/dist/db.js
|
|
2284
2323
|
import { DatabaseSync } from "node:sqlite";
|
|
2324
|
+
var WAL_SIZE_LIMIT = 64 * 1024 * 1024;
|
|
2285
2325
|
function openRaw(file) {
|
|
2286
2326
|
const db = new DatabaseSync(file);
|
|
2287
2327
|
db.exec("PRAGMA journal_mode = WAL");
|
|
2328
|
+
db.exec(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT}`);
|
|
2288
2329
|
db.exec("PRAGMA foreign_keys = ON");
|
|
2289
2330
|
return db;
|
|
2290
2331
|
}
|
|
@@ -2304,7 +2345,7 @@ function openDb(file) {
|
|
|
2304
2345
|
applySchema(db);
|
|
2305
2346
|
return db;
|
|
2306
2347
|
}
|
|
2307
|
-
function resetSchema(db) {
|
|
2348
|
+
function resetSchema(db, { bulk = false } = {}) {
|
|
2308
2349
|
db.exec("PRAGMA foreign_keys = OFF");
|
|
2309
2350
|
try {
|
|
2310
2351
|
db.exec(`
|
|
@@ -2331,10 +2372,20 @@ function resetSchema(db) {
|
|
|
2331
2372
|
DROP TABLE IF EXISTS meta;
|
|
2332
2373
|
`);
|
|
2333
2374
|
applySchema(db);
|
|
2375
|
+
if (bulk) {
|
|
2376
|
+
for (const name of BULK_DEFERRED.indexes)
|
|
2377
|
+
db.exec(`DROP INDEX IF EXISTS ${name}`);
|
|
2378
|
+
for (const name of BULK_DEFERRED.triggers)
|
|
2379
|
+
db.exec(`DROP TRIGGER IF EXISTS ${name}`);
|
|
2380
|
+
}
|
|
2334
2381
|
} finally {
|
|
2335
2382
|
db.exec("PRAGMA foreign_keys = ON");
|
|
2336
2383
|
}
|
|
2337
2384
|
}
|
|
2385
|
+
function finishBulkLoad(db) {
|
|
2386
|
+
db.exec(SCHEMA_SQL);
|
|
2387
|
+
db.exec("INSERT INTO events_fts(events_fts) VALUES('rebuild')");
|
|
2388
|
+
}
|
|
2338
2389
|
|
|
2339
2390
|
// ../ingest/dist/adapters/claude-code.js
|
|
2340
2391
|
import { readdirSync as readdirSync2 } from "node:fs";
|
|
@@ -3643,6 +3694,13 @@ function nearestGitRoot(path3, home) {
|
|
|
3643
3694
|
}
|
|
3644
3695
|
return null;
|
|
3645
3696
|
}
|
|
3697
|
+
function sessionCwd(first, last) {
|
|
3698
|
+
const f = strip(first);
|
|
3699
|
+
const l = strip(last);
|
|
3700
|
+
if (f === l || existsSync4(l))
|
|
3701
|
+
return last;
|
|
3702
|
+
return l.startsWith(f === "/" ? "/" : f + "/") ? first : last;
|
|
3703
|
+
}
|
|
3646
3704
|
function canonicalizeProjects(db, opts = {}) {
|
|
3647
3705
|
const home = strip(opts.homeDir ?? homedir3());
|
|
3648
3706
|
const rows = db.prepare("SELECT id, agent_id, path FROM projects").all();
|
|
@@ -3729,6 +3787,77 @@ function xmlTag(text, tag) {
|
|
|
3729
3787
|
return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
|
|
3730
3788
|
}
|
|
3731
3789
|
|
|
3790
|
+
// ../ingest/dist/skillslots.js
|
|
3791
|
+
var ARG_TRAILERS = ["\nARGUMENTS:", "\n## User Request\n"];
|
|
3792
|
+
var EMPTY_OUTPUT = "(Bash completed with no output)";
|
|
3793
|
+
var DYNAMIC_PLACEHOLDER = "(dynamic context: command output omitted)";
|
|
3794
|
+
var isMarker = (t) => t === EMPTY_OUTPUT || t === DYNAMIC_PLACEHOLDER;
|
|
3795
|
+
function uniqueLine(lines, text) {
|
|
3796
|
+
let at = -1;
|
|
3797
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3798
|
+
if (lines[i].trim() !== text)
|
|
3799
|
+
continue;
|
|
3800
|
+
if (at >= 0)
|
|
3801
|
+
return -1;
|
|
3802
|
+
at = i;
|
|
3803
|
+
}
|
|
3804
|
+
return at;
|
|
3805
|
+
}
|
|
3806
|
+
function learnSlots(body) {
|
|
3807
|
+
const lines = body.split("\n");
|
|
3808
|
+
const trimmed = lines.map((l) => l.trim());
|
|
3809
|
+
const slots = [];
|
|
3810
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3811
|
+
if (!isMarker(trimmed[i]))
|
|
3812
|
+
continue;
|
|
3813
|
+
let b = i - 1;
|
|
3814
|
+
while (b >= 0 && trimmed[b] === "")
|
|
3815
|
+
b--;
|
|
3816
|
+
let a = i + 1;
|
|
3817
|
+
while (a < lines.length && (trimmed[a] === "" || isMarker(trimmed[a])))
|
|
3818
|
+
a++;
|
|
3819
|
+
i = a - 1;
|
|
3820
|
+
if (b < 0 || a >= lines.length)
|
|
3821
|
+
continue;
|
|
3822
|
+
const slot = { before: trimmed[b], after: trimmed[a] };
|
|
3823
|
+
if (uniqueLine(lines, slot.before) < 0 || uniqueLine(lines, slot.after) < 0)
|
|
3824
|
+
continue;
|
|
3825
|
+
if (!slots.some((s) => s.before === slot.before && s.after === slot.after))
|
|
3826
|
+
slots.push(slot);
|
|
3827
|
+
}
|
|
3828
|
+
return slots;
|
|
3829
|
+
}
|
|
3830
|
+
function mergeSlots(into, body) {
|
|
3831
|
+
for (const s of learnSlots(body))
|
|
3832
|
+
if (!into.some((x) => x.before === s.before && x.after === s.after))
|
|
3833
|
+
into.push(s);
|
|
3834
|
+
return into;
|
|
3835
|
+
}
|
|
3836
|
+
function maskDynamic(body, slots) {
|
|
3837
|
+
let lines = body.split("\n");
|
|
3838
|
+
for (const { before, after } of slots) {
|
|
3839
|
+
const b = uniqueLine(lines, before);
|
|
3840
|
+
const a = uniqueLine(lines, after);
|
|
3841
|
+
if (b < 0 || a <= b)
|
|
3842
|
+
continue;
|
|
3843
|
+
lines = [...lines.slice(0, b + 1), "", DYNAMIC_PLACEHOLDER, "", ...lines.slice(a)];
|
|
3844
|
+
}
|
|
3845
|
+
return lines.join("\n");
|
|
3846
|
+
}
|
|
3847
|
+
var MIN_MASKED_ARGS = 8;
|
|
3848
|
+
function maskKnownValues(body, known) {
|
|
3849
|
+
const subs = [];
|
|
3850
|
+
const args = known.args?.trim();
|
|
3851
|
+
if (args && args.length >= MIN_MASKED_ARGS)
|
|
3852
|
+
subs.push([args, "$ARGUMENTS"]);
|
|
3853
|
+
if (known.baseDir)
|
|
3854
|
+
subs.push([known.baseDir, "${CLAUDE_SKILL_DIR}"]);
|
|
3855
|
+
if (known.sessionId)
|
|
3856
|
+
subs.push([known.sessionId, "${CLAUDE_SESSION_ID}"]);
|
|
3857
|
+
subs.sort((x, y) => y[0].length - x[0].length);
|
|
3858
|
+
return subs.reduce((out, [value, placeholder]) => out.split(value).join(placeholder), body);
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3732
3861
|
// ../ingest/dist/pipeline.js
|
|
3733
3862
|
function newStats() {
|
|
3734
3863
|
return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
|
|
@@ -3750,9 +3879,15 @@ function parseSkillInjection(text) {
|
|
|
3750
3879
|
const baseDir = firstLine || null;
|
|
3751
3880
|
let body = firstNl >= 0 ? text.slice(firstNl + 1) : "";
|
|
3752
3881
|
let args = null;
|
|
3753
|
-
|
|
3882
|
+
let argIdx = -1;
|
|
3883
|
+
let trailer = "";
|
|
3884
|
+
for (const t of ARG_TRAILERS) {
|
|
3885
|
+
const i = body.lastIndexOf(t);
|
|
3886
|
+
if (i > argIdx)
|
|
3887
|
+
[argIdx, trailer] = [i, t];
|
|
3888
|
+
}
|
|
3754
3889
|
if (argIdx >= 0) {
|
|
3755
|
-
args = body.slice(argIdx +
|
|
3890
|
+
args = body.slice(argIdx + trailer.length).trim() || null;
|
|
3756
3891
|
body = body.slice(0, argIdx);
|
|
3757
3892
|
}
|
|
3758
3893
|
return { baseDir, nameKey: baseDir ? skillNameKey(baseDir) : "", body: body.trim(), args };
|
|
@@ -3822,6 +3957,10 @@ function durationMs(start, end) {
|
|
|
3822
3957
|
return null;
|
|
3823
3958
|
return Math.max(0, b - a);
|
|
3824
3959
|
}
|
|
3960
|
+
function isTurnStart(e) {
|
|
3961
|
+
const body = e.text?.trim() ?? "";
|
|
3962
|
+
return e.type === "user" && e.is_meta === 0 && body.length > 0 && !isCommandResultCarrier(body);
|
|
3963
|
+
}
|
|
3825
3964
|
function buildTurns(sessionId, rows) {
|
|
3826
3965
|
rows.sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || (x.seq ?? 0) - (y.seq ?? 0) || x.uuid.localeCompare(y.uuid));
|
|
3827
3966
|
const turns = [];
|
|
@@ -3829,9 +3968,7 @@ function buildTurns(sessionId, rows) {
|
|
|
3829
3968
|
let cur = null;
|
|
3830
3969
|
let seq = 0;
|
|
3831
3970
|
for (const e of rows) {
|
|
3832
|
-
|
|
3833
|
-
const isPrompt = e.type === "user" && e.is_meta === 0 && body.length > 0 && !isCommandResultCarrier(body);
|
|
3834
|
-
if (isPrompt) {
|
|
3971
|
+
if (isTurnStart(e)) {
|
|
3835
3972
|
cur = {
|
|
3836
3973
|
id: `${sessionId}:${seq}`,
|
|
3837
3974
|
session_id: sessionId,
|
|
@@ -3888,7 +4025,7 @@ function expandDirtyScope(db, dirty) {
|
|
|
3888
4025
|
}
|
|
3889
4026
|
}
|
|
3890
4027
|
function rebuildTurns(db, scope, sessionIds) {
|
|
3891
|
-
const selEvents = db.prepare("SELECT uuid, seq, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?");
|
|
4028
|
+
const selEvents = db.prepare("SELECT uuid, seq, type, is_sidechain, is_meta, timestamp, model, text, turn_id FROM events WHERE session_id = ?");
|
|
3892
4029
|
const insTurn = db.prepare(`INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)
|
|
3893
4030
|
VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`);
|
|
3894
4031
|
const updEventTurn = db.prepare("UPDATE events SET turn_id = ? WHERE uuid = ?");
|
|
@@ -3898,8 +4035,11 @@ function rebuildTurns(db, scope, sessionIds) {
|
|
|
3898
4035
|
const { turns, eventTurn } = buildTurns(id, rows);
|
|
3899
4036
|
for (const t of turns)
|
|
3900
4037
|
runNamed(insTurn, t);
|
|
3901
|
-
for (const
|
|
3902
|
-
|
|
4038
|
+
for (const r of rows) {
|
|
4039
|
+
const turnId = eventTurn.get(r.uuid) ?? null;
|
|
4040
|
+
if (turnId !== r.turn_id)
|
|
4041
|
+
updEventTurn.run(turnId, r.uuid);
|
|
4042
|
+
}
|
|
3903
4043
|
}
|
|
3904
4044
|
});
|
|
3905
4045
|
tx();
|
|
@@ -3916,6 +4056,15 @@ function linkSkillVersions(db, sessionIds) {
|
|
|
3916
4056
|
first_seen = MIN(first_seen, excluded.first_seen),
|
|
3917
4057
|
base_dir = excluded.base_dir`);
|
|
3918
4058
|
const linkCall = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE id = ?");
|
|
4059
|
+
const selLinkedVersions = db.prepare("SELECT id, base_dir, body, first_seen, last_seen FROM skills WHERE name = ? AND id IN (SELECT skill_id FROM tool_calls WHERE skill_id IS NOT NULL)");
|
|
4060
|
+
const upsertRemasked = db.prepare(`INSERT INTO skills (id, name, base_dir, body, summary, body_bytes, first_seen, last_seen)
|
|
4061
|
+
VALUES (@id, @name, @base_dir, @body, @summary, @body_bytes, @first_seen, @last_seen)
|
|
4062
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
4063
|
+
last_seen = MAX(last_seen, excluded.last_seen),
|
|
4064
|
+
first_seen = MIN(first_seen, excluded.first_seen)`);
|
|
4065
|
+
const relinkVersion = db.prepare("UPDATE tool_calls SET skill_id = ? WHERE skill_id = ?");
|
|
4066
|
+
const delVersion = db.prepare("DELETE FROM skills WHERE id = ?");
|
|
4067
|
+
const matches = [];
|
|
3919
4068
|
const linkTx = transaction(db, () => {
|
|
3920
4069
|
for (const { id: sid } of sessionIds) {
|
|
3921
4070
|
const calls = selSkillCalls.all(sid);
|
|
@@ -3965,18 +4114,53 @@ function linkSkillVersions(db, sessionIds) {
|
|
|
3965
4114
|
idx = 0;
|
|
3966
4115
|
const [match] = queue.splice(idx, 1);
|
|
3967
4116
|
const c = calls.find((x) => x.id === match.id);
|
|
3968
|
-
const
|
|
3969
|
-
|
|
3970
|
-
|
|
4117
|
+
const body = maskKnownValues(inj.body, { args: match.args ?? inj.args, baseDir: inj.baseDir, sessionId: sid });
|
|
4118
|
+
matches.push({ callId: match.id, name: c.skill_name, baseDir: inj.baseDir, body, ts: e.timestamp });
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
const slotsByName = /* @__PURE__ */ new Map();
|
|
4122
|
+
const addSlots = (name, body) => slotsByName.set(name, mergeSlots(slotsByName.get(name) ?? [], body));
|
|
4123
|
+
for (const m of matches)
|
|
4124
|
+
addSlots(m.name, m.body);
|
|
4125
|
+
for (const name of slotsByName.keys()) {
|
|
4126
|
+
for (const v of selLinkedVersions.all(name))
|
|
4127
|
+
addSlots(name, v.body);
|
|
4128
|
+
}
|
|
4129
|
+
for (const m of matches) {
|
|
4130
|
+
const body = maskDynamic(m.body, slotsByName.get(m.name));
|
|
4131
|
+
const vid = skillVersionId(m.name, body);
|
|
4132
|
+
upsertSkill.run({
|
|
4133
|
+
id: vid,
|
|
4134
|
+
name: m.name,
|
|
4135
|
+
base_dir: m.baseDir,
|
|
4136
|
+
body,
|
|
4137
|
+
summary: skillSummary(body),
|
|
4138
|
+
body_bytes: Buffer.byteLength(body, "utf8"),
|
|
4139
|
+
ts: m.ts
|
|
4140
|
+
});
|
|
4141
|
+
linkCall.run(vid, m.callId);
|
|
4142
|
+
}
|
|
4143
|
+
for (const [name, slots] of slotsByName) {
|
|
4144
|
+
if (!slots.length)
|
|
4145
|
+
continue;
|
|
4146
|
+
const versions = selLinkedVersions.all(name);
|
|
4147
|
+
for (const v of versions) {
|
|
4148
|
+
const body = maskDynamic(v.body, slots);
|
|
4149
|
+
if (body === v.body)
|
|
4150
|
+
continue;
|
|
4151
|
+
const vid = skillVersionId(name, body);
|
|
4152
|
+
upsertRemasked.run({
|
|
3971
4153
|
id: vid,
|
|
3972
4154
|
name,
|
|
3973
|
-
base_dir:
|
|
3974
|
-
body
|
|
3975
|
-
summary: skillSummary(
|
|
3976
|
-
body_bytes: Buffer.byteLength(
|
|
3977
|
-
|
|
4155
|
+
base_dir: v.base_dir,
|
|
4156
|
+
body,
|
|
4157
|
+
summary: skillSummary(body),
|
|
4158
|
+
body_bytes: Buffer.byteLength(body, "utf8"),
|
|
4159
|
+
first_seen: v.first_seen,
|
|
4160
|
+
last_seen: v.last_seen
|
|
3978
4161
|
});
|
|
3979
|
-
|
|
4162
|
+
relinkVersion.run(vid, v.id);
|
|
4163
|
+
delVersion.run(v.id);
|
|
3980
4164
|
}
|
|
3981
4165
|
}
|
|
3982
4166
|
});
|
|
@@ -4035,26 +4219,28 @@ function rebuildDerived(db, dirty) {
|
|
|
4035
4219
|
andId: incremental ? " AND id IN (SELECT id FROM _dirty)" : "",
|
|
4036
4220
|
andSelfId: incremental ? " AND sessions.id IN (SELECT id FROM _dirty)" : ""
|
|
4037
4221
|
};
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4222
|
+
return transaction(db, () => {
|
|
4223
|
+
if (dirty != null)
|
|
4224
|
+
expandDirtyScope(db, dirty);
|
|
4225
|
+
db.exec("PRAGMA defer_foreign_keys = ON");
|
|
4226
|
+
db.exec(`UPDATE token_usage SET turn_id = NULL${scope.bySession}`);
|
|
4227
|
+
db.exec(`UPDATE tool_calls SET turn_id = NULL${scope.bySession}`);
|
|
4228
|
+
db.exec(`UPDATE tool_calls SET skill_id = NULL${scope.bySession}`);
|
|
4229
|
+
db.exec(`UPDATE sessions SET parent_turn_id = NULL${scope.byId}`);
|
|
4230
|
+
db.exec(`DELETE FROM turns${scope.bySession}`);
|
|
4231
|
+
const sessionIds = db.prepare(`SELECT id FROM sessions${scope.byId}`).all();
|
|
4232
|
+
rebuildTurns(db, scope, sessionIds);
|
|
4233
|
+
linkSkillVersions(db, sessionIds);
|
|
4234
|
+
linkParents(db, scope);
|
|
4235
|
+
recomputeSessionAggregates(db, scope);
|
|
4236
|
+
pruneOrphans(db, scope, incremental);
|
|
4237
|
+
if (incremental) {
|
|
4238
|
+
const expanded = new Set(db.prepare("SELECT id FROM _dirty").all().map((r) => r.id));
|
|
4239
|
+
db.exec("DROP TABLE IF EXISTS _dirty");
|
|
4240
|
+
return expanded;
|
|
4241
|
+
}
|
|
4242
|
+
return null;
|
|
4243
|
+
})();
|
|
4058
4244
|
}
|
|
4059
4245
|
function prepareStatements(db) {
|
|
4060
4246
|
return {
|
|
@@ -4111,9 +4297,11 @@ function prepareStatements(db) {
|
|
|
4111
4297
|
workflow_run_id = COALESCE(excluded.workflow_run_id, workflow_run_id)`)
|
|
4112
4298
|
};
|
|
4113
4299
|
}
|
|
4114
|
-
function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
4300
|
+
function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats, opts = {}) {
|
|
4115
4301
|
const sessionMeta = {};
|
|
4116
4302
|
let eventsInFile = 0;
|
|
4303
|
+
let turnSeq = -1;
|
|
4304
|
+
let firstCwd = null;
|
|
4117
4305
|
const tx = transaction(db, () => {
|
|
4118
4306
|
stmts.insSessionStub.run(file.sessionId, adapter.agentId, file.sourceId);
|
|
4119
4307
|
let seq = 0;
|
|
@@ -4128,10 +4316,19 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
4128
4316
|
continue;
|
|
4129
4317
|
}
|
|
4130
4318
|
const parsed = adapter.parseLine(raw, file, seq++);
|
|
4131
|
-
if (parsed.meta)
|
|
4319
|
+
if (parsed.meta) {
|
|
4320
|
+
if (parsed.meta.cwd && firstCwd === null)
|
|
4321
|
+
firstCwd = parsed.meta.cwd;
|
|
4132
4322
|
Object.assign(sessionMeta, parsed.meta);
|
|
4323
|
+
}
|
|
4133
4324
|
if (parsed.event) {
|
|
4134
|
-
|
|
4325
|
+
let turnId = parsed.event.turn_id;
|
|
4326
|
+
if (opts.provisionalTurns) {
|
|
4327
|
+
if (isTurnStart(parsed.event))
|
|
4328
|
+
turnSeq++;
|
|
4329
|
+
turnId = turnSeq < 0 ? null : `${file.sessionId}:${turnSeq}`;
|
|
4330
|
+
}
|
|
4331
|
+
const info = stmts.insEvent.run({ ...parsed.event, turn_id: turnId, raw_json: packRaw(parsed.event.raw_json) });
|
|
4135
4332
|
if (info.changes > 0) {
|
|
4136
4333
|
stats.newEvents++;
|
|
4137
4334
|
eventsInFile++;
|
|
@@ -4160,11 +4357,12 @@ function ingestFile(db, stmts, adapter, file, lines, meta, now2, stats) {
|
|
|
4160
4357
|
}
|
|
4161
4358
|
let projectId2 = null;
|
|
4162
4359
|
if (sessionMeta.cwd) {
|
|
4163
|
-
|
|
4360
|
+
const cwd = sessionCwd(firstCwd ?? sessionMeta.cwd, sessionMeta.cwd);
|
|
4361
|
+
projectId2 = createHash5("sha1").update(`${adapter.agentId}\0${cwd}`).digest("hex").slice(0, 16);
|
|
4164
4362
|
stmts.upsertProject.run({
|
|
4165
4363
|
id: projectId2,
|
|
4166
4364
|
agent_id: adapter.agentId,
|
|
4167
|
-
path:
|
|
4365
|
+
path: cwd,
|
|
4168
4366
|
encoded_dir: file.encodedDir || null,
|
|
4169
4367
|
now: now2
|
|
4170
4368
|
});
|
|
@@ -4503,6 +4701,13 @@ function shouldReingest(path3, size, mtimeMs, prev) {
|
|
|
4503
4701
|
return { action: "skip-hash", hash: hash2 };
|
|
4504
4702
|
return { action: "ingest", hash: hash2, buf };
|
|
4505
4703
|
}
|
|
4704
|
+
function assertForeignKeys(db) {
|
|
4705
|
+
const bad = db.prepare("PRAGMA foreign_key_check").all();
|
|
4706
|
+
if (!bad.length)
|
|
4707
|
+
return;
|
|
4708
|
+
const edges = [...new Set(bad.map((b) => `${b.table} -> ${b.parent}`))].join(", ");
|
|
4709
|
+
throw new Error(`agent-lens-ingest: --full produced ${bad.length} foreign-key violations (${edges}); rolled back`);
|
|
4710
|
+
}
|
|
4506
4711
|
function printIngestReport(db, dbPath, r) {
|
|
4507
4712
|
const count = (sql) => db.prepare(sql).get().n;
|
|
4508
4713
|
const sessions = count("SELECT COUNT(*) n FROM sessions");
|
|
@@ -4549,9 +4754,7 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4549
4754
|
}
|
|
4550
4755
|
mkdirSync4(dirname6(dbPath), { recursive: true });
|
|
4551
4756
|
const db = args.full ? openRaw(dbPath) : openDb(dbPath);
|
|
4552
|
-
if (args.full) {
|
|
4553
|
-
resetSchema(db);
|
|
4554
|
-
} else {
|
|
4757
|
+
if (!args.full) {
|
|
4555
4758
|
const dbVer = readSchemaVersion(db);
|
|
4556
4759
|
if (dbVer != null && dbVer !== SCHEMA_VERSION) {
|
|
4557
4760
|
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.`);
|
|
@@ -4565,62 +4768,81 @@ function runIngest(argv = process.argv.slice(2)) {
|
|
|
4565
4768
|
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4566
4769
|
const excludePaths = loadExcludes();
|
|
4567
4770
|
const excludedDirs = parseExcludes(excludePaths.join(","));
|
|
4568
|
-
const pruned = pruneExcluded(db, excludePaths);
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4771
|
+
const pruned = args.full ? 0 : pruneExcluded(db, excludePaths);
|
|
4772
|
+
if (args.full)
|
|
4773
|
+
db.exec("PRAGMA foreign_keys = OFF");
|
|
4774
|
+
let report;
|
|
4775
|
+
try {
|
|
4776
|
+
report = transaction(db, () => {
|
|
4777
|
+
if (args.full)
|
|
4778
|
+
resetSchema(db, { bulk: true });
|
|
4779
|
+
const stmts = prepareStatements(db);
|
|
4780
|
+
const stats = newStats();
|
|
4781
|
+
const wfStats = newWorkflowStats();
|
|
4782
|
+
const metaStats = newMetaStats();
|
|
4783
|
+
const trStats = newToolResultStats();
|
|
4784
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
4785
|
+
for (const source of sources) {
|
|
4786
|
+
const adapter = adapterById.get(source.agent);
|
|
4787
|
+
if (!adapter) {
|
|
4788
|
+
console.warn(`agent-lens-ingest: no adapter for agent '${source.agent}' (source '${source.label}') \u2014 skipping`);
|
|
4789
|
+
continue;
|
|
4790
|
+
}
|
|
4791
|
+
stmts.insAgent.run(adapter.agentId, adapter.agentName);
|
|
4792
|
+
stmts.insSource.run({ id: source.label, label: source.label, agent_id: adapter.agentId, config_dir: source.configDir });
|
|
4793
|
+
let files = adapter.discover(join9(archiveRoot, source.label), source.label);
|
|
4794
|
+
if (excludedDirs.length)
|
|
4795
|
+
files = files.filter((f) => !isExcludedArchivePath(f.path, excludedDirs));
|
|
4796
|
+
files.sort((a, b) => Number(a.isVersion) - Number(b.isVersion));
|
|
4797
|
+
for (const file of files) {
|
|
4798
|
+
stats.files++;
|
|
4799
|
+
const st = statSync3(file.path);
|
|
4800
|
+
const mtimeMs = Math.trunc(st.mtimeMs);
|
|
4801
|
+
const prev = args.full ? void 0 : stmts.getState.get(file.path);
|
|
4802
|
+
const decision = shouldReingest(file.path, st.size, mtimeMs, prev);
|
|
4803
|
+
if (decision.action === "skip-stat") {
|
|
4804
|
+
stats.skipped++;
|
|
4805
|
+
continue;
|
|
4806
|
+
}
|
|
4807
|
+
if (decision.action === "skip-hash") {
|
|
4808
|
+
stmts.setState.run({
|
|
4809
|
+
file_path: file.path,
|
|
4810
|
+
size: st.size,
|
|
4811
|
+
mtime_ms: mtimeMs,
|
|
4812
|
+
sha256: decision.hash,
|
|
4813
|
+
events_ingested: prev.events_ingested,
|
|
4814
|
+
ingested_at: now2
|
|
4815
|
+
});
|
|
4816
|
+
stats.skipped++;
|
|
4817
|
+
continue;
|
|
4818
|
+
}
|
|
4819
|
+
const lines = decision.buf ? decision.buf.toString("utf8").split("\n") : streamLines(file.path);
|
|
4820
|
+
ingestFile(db, stmts, adapter, file, lines, { size: st.size, mtimeMs, hash: decision.hash }, now2, stats, { provisionalTurns: args.full });
|
|
4821
|
+
dirty.add(file.sessionId);
|
|
4822
|
+
}
|
|
4823
|
+
ingestWorkflowResults(db, join9(archiveRoot, source.label), source.label, excludedDirs, now2, wfStats, args.full);
|
|
4824
|
+
ingestSubagentMeta(db, join9(archiveRoot, source.label), source.label, excludedDirs, now2, metaStats, args.full);
|
|
4825
|
+
ingestToolResults(db, join9(archiveRoot, source.label), excludedDirs, now2, trStats, args.full);
|
|
4608
4826
|
}
|
|
4609
|
-
const
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4827
|
+
const expanded = rebuildDerived(db, args.full ? null : dirty);
|
|
4828
|
+
if (args.full)
|
|
4829
|
+
finishBulkLoad(db);
|
|
4830
|
+
canonicalizeProjects(db);
|
|
4831
|
+
const classified = classify(db, args.full ? null : expanded);
|
|
4832
|
+
const detected = detect(db, args.full ? null : expanded);
|
|
4833
|
+
classifyErrors(db, args.full ? null : expanded);
|
|
4834
|
+
const fileChanges = deriveFileChanges(db, args.full ? null : expanded);
|
|
4835
|
+
db.exec(ANALYZE_SQL);
|
|
4836
|
+
if (args.full)
|
|
4837
|
+
assertForeignKeys(db);
|
|
4838
|
+
return { stats, classified, detected, fileChanges, wfStats, metaStats, trStats };
|
|
4839
|
+
})();
|
|
4840
|
+
} finally {
|
|
4841
|
+
if (args.full)
|
|
4842
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
4616
4843
|
}
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
const classified = classify(db, args.full ? null : expanded);
|
|
4620
|
-
const detected = detect(db, args.full ? null : expanded);
|
|
4621
|
-
classifyErrors(db, args.full ? null : expanded);
|
|
4622
|
-
const fileChanges = deriveFileChanges(db, args.full ? null : expanded);
|
|
4623
|
-
printIngestReport(db, dbPath, { stats, pruned, classified, detected, fileChanges, wfStats, metaStats, trStats });
|
|
4844
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
4845
|
+
printIngestReport(db, dbPath, { ...report, pruned });
|
|
4624
4846
|
}
|
|
4625
4847
|
function runMetrics(argv = process.argv.slice(2)) {
|
|
4626
4848
|
let dbArg = "";
|
|
@@ -4739,9 +4961,11 @@ function costOf(rows) {
|
|
|
4739
4961
|
}
|
|
4740
4962
|
|
|
4741
4963
|
// ../server/dist/db.js
|
|
4964
|
+
var READ_CACHE_KIB = 64 * 1024;
|
|
4742
4965
|
function openReadonly(file) {
|
|
4743
4966
|
const db = new DatabaseSync2(file, { readOnly: true });
|
|
4744
4967
|
db.exec("PRAGMA query_only = ON");
|
|
4968
|
+
db.exec(`PRAGMA cache_size = -${READ_CACHE_KIB}`);
|
|
4745
4969
|
return db;
|
|
4746
4970
|
}
|
|
4747
4971
|
function schemaStatus(db) {
|
|
@@ -6161,6 +6385,31 @@ function setPref(db, key, value) {
|
|
|
6161
6385
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`).run(key, value, (/* @__PURE__ */ new Date()).toISOString());
|
|
6162
6386
|
}
|
|
6163
6387
|
|
|
6388
|
+
// ../server/dist/response-cache.js
|
|
6389
|
+
function dataVersionCache(db, max = 64) {
|
|
6390
|
+
const version2 = db.prepare("PRAGMA data_version");
|
|
6391
|
+
const entries = /* @__PURE__ */ new Map();
|
|
6392
|
+
let seen;
|
|
6393
|
+
return (key, compute) => {
|
|
6394
|
+
const v = version2.get().data_version;
|
|
6395
|
+
if (v !== seen) {
|
|
6396
|
+
entries.clear();
|
|
6397
|
+
seen = v;
|
|
6398
|
+
}
|
|
6399
|
+
if (entries.has(key)) {
|
|
6400
|
+
const hit = entries.get(key);
|
|
6401
|
+
entries.delete(key);
|
|
6402
|
+
entries.set(key, hit);
|
|
6403
|
+
return hit;
|
|
6404
|
+
}
|
|
6405
|
+
const value = compute();
|
|
6406
|
+
entries.set(key, value);
|
|
6407
|
+
if (entries.size > max)
|
|
6408
|
+
entries.delete(entries.keys().next().value);
|
|
6409
|
+
return value;
|
|
6410
|
+
};
|
|
6411
|
+
}
|
|
6412
|
+
|
|
6164
6413
|
// ../server/dist/app.js
|
|
6165
6414
|
function notFound(reply, message = "not found") {
|
|
6166
6415
|
return reply.code(404).send({ error: { code: "NOT_FOUND", message } });
|
|
@@ -6239,19 +6488,20 @@ async function createApp(db, opts = {}) {
|
|
|
6239
6488
|
const q = req.query;
|
|
6240
6489
|
return { source: q.source, from: q.from, to: q.to, models: q.models?.split(",").filter(Boolean) };
|
|
6241
6490
|
};
|
|
6242
|
-
|
|
6491
|
+
const cached2 = dataVersionCache(db);
|
|
6492
|
+
app.get("/api/dashboard/overview", async (req) => cached2(req.url, () => dashboardOverview(db, dashFilters(req))));
|
|
6243
6493
|
app.get("/api/dashboard/timeseries", async (req) => {
|
|
6244
6494
|
const q = req.query;
|
|
6245
|
-
return dashboardTimeseries(db, dashFilters(req), q.bucket);
|
|
6495
|
+
return cached2(req.url, () => dashboardTimeseries(db, dashFilters(req), q.bucket));
|
|
6246
6496
|
});
|
|
6247
|
-
app.get("/api/dashboard/breakdowns", async (req) => dashboardBreakdowns(db, dashFilters(req)));
|
|
6497
|
+
app.get("/api/dashboard/breakdowns", async (req) => cached2(req.url, () => dashboardBreakdowns(db, dashFilters(req))));
|
|
6248
6498
|
app.get("/api/dashboard/time", async (req) => {
|
|
6249
6499
|
const q = req.query;
|
|
6250
|
-
return dashboardTime(db, dashFilters(req), q.bucket);
|
|
6500
|
+
return cached2(req.url, () => dashboardTime(db, dashFilters(req), q.bucket));
|
|
6251
6501
|
});
|
|
6252
6502
|
app.get("/api/dashboard/audit", async (req) => {
|
|
6253
6503
|
const q = req.query;
|
|
6254
|
-
return dashboardAudit(db, dashFilters(req), q.bucket);
|
|
6504
|
+
return cached2(req.url, () => dashboardAudit(db, dashFilters(req), q.bucket));
|
|
6255
6505
|
});
|
|
6256
6506
|
const PREF_KEY = /^[a-z0-9._-]{1,64}$/i;
|
|
6257
6507
|
app.get("/api/prefs/:key", async (req, reply) => {
|