agent-trace 0.2.5 → 0.2.7
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/agent-trace.cjs +189 -3
- package/package.json +1 -1
package/agent-trace.cjs
CHANGED
|
@@ -24988,7 +24988,7 @@ async function startDashboardServer(options = {}) {
|
|
|
24988
24988
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
24989
24989
|
var SCHEMA_SQL = `
|
|
24990
24990
|
CREATE TABLE IF NOT EXISTS agent_events (
|
|
24991
|
-
event_id TEXT NOT NULL,
|
|
24991
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
24992
24992
|
event_type TEXT NOT NULL,
|
|
24993
24993
|
event_timestamp TEXT NOT NULL,
|
|
24994
24994
|
session_id TEXT NOT NULL,
|
|
@@ -25100,7 +25100,9 @@ var SqliteClient = class {
|
|
|
25100
25100
|
this.db = new import_better_sqlite3.default(dbPath);
|
|
25101
25101
|
this.db.pragma("journal_mode = WAL");
|
|
25102
25102
|
this.db.pragma("synchronous = NORMAL");
|
|
25103
|
+
this.migrateDeduplicateEvents();
|
|
25103
25104
|
this.db.exec(SCHEMA_SQL);
|
|
25105
|
+
this.migrateRebuildBrokenTraces();
|
|
25104
25106
|
}
|
|
25105
25107
|
async insertJsonEachRow(request) {
|
|
25106
25108
|
if (request.rows.length === 0) return;
|
|
@@ -25319,9 +25321,181 @@ var SqliteClient = class {
|
|
|
25319
25321
|
close() {
|
|
25320
25322
|
this.db.close();
|
|
25321
25323
|
}
|
|
25324
|
+
/**
|
|
25325
|
+
* Migration: deduplicate agent_events rows from older schemas that lacked a UNIQUE constraint.
|
|
25326
|
+
* Runs once — if the old table exists without a unique index, it rebuilds it.
|
|
25327
|
+
*/
|
|
25328
|
+
migrateDeduplicateEvents() {
|
|
25329
|
+
const tableExists = this.db.prepare(
|
|
25330
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='agent_events'"
|
|
25331
|
+
).get();
|
|
25332
|
+
if (tableExists === void 0) {
|
|
25333
|
+
return;
|
|
25334
|
+
}
|
|
25335
|
+
const hasUniqueIndex = this.db.prepare(
|
|
25336
|
+
"SELECT 1 FROM sqlite_master WHERE type='index' AND tbl_name='agent_events' AND sql LIKE '%UNIQUE%'"
|
|
25337
|
+
).get();
|
|
25338
|
+
const indexInfo = this.db.prepare("PRAGMA index_list('agent_events')").all();
|
|
25339
|
+
const hasAutoUnique = indexInfo.some((idx) => idx["unique"] === 1);
|
|
25340
|
+
if (hasUniqueIndex !== void 0 || hasAutoUnique) {
|
|
25341
|
+
return;
|
|
25342
|
+
}
|
|
25343
|
+
const countResult = this.db.prepare(
|
|
25344
|
+
"SELECT COUNT(*) as total FROM agent_events"
|
|
25345
|
+
).get();
|
|
25346
|
+
const distinctResult = this.db.prepare(
|
|
25347
|
+
"SELECT COUNT(DISTINCT event_id) as distinct_count FROM agent_events"
|
|
25348
|
+
).get();
|
|
25349
|
+
const total = countResult?.total ?? 0;
|
|
25350
|
+
const distinct = distinctResult?.distinct_count ?? 0;
|
|
25351
|
+
if (total === 0) {
|
|
25352
|
+
this.db.exec("DROP TABLE agent_events");
|
|
25353
|
+
const tracesExist = this.db.prepare(
|
|
25354
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
|
|
25355
|
+
).get();
|
|
25356
|
+
if (tracesExist !== void 0) {
|
|
25357
|
+
this.db.exec("DELETE FROM session_traces");
|
|
25358
|
+
}
|
|
25359
|
+
return;
|
|
25360
|
+
}
|
|
25361
|
+
console.log(`[agent-trace] migrating: deduplicating agent_events (${total} rows \u2192 ${distinct} distinct)`);
|
|
25362
|
+
this.db.exec(`
|
|
25363
|
+
CREATE TABLE agent_events_dedup (
|
|
25364
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
25365
|
+
event_type TEXT NOT NULL,
|
|
25366
|
+
event_timestamp TEXT NOT NULL,
|
|
25367
|
+
session_id TEXT NOT NULL,
|
|
25368
|
+
prompt_id TEXT,
|
|
25369
|
+
user_id TEXT NOT NULL DEFAULT 'unknown_user',
|
|
25370
|
+
source TEXT NOT NULL DEFAULT 'hook',
|
|
25371
|
+
agent_type TEXT NOT NULL DEFAULT 'claude_code',
|
|
25372
|
+
tool_name TEXT,
|
|
25373
|
+
tool_success INTEGER,
|
|
25374
|
+
tool_duration_ms REAL,
|
|
25375
|
+
model TEXT,
|
|
25376
|
+
cost_usd REAL,
|
|
25377
|
+
input_tokens INTEGER,
|
|
25378
|
+
output_tokens INTEGER,
|
|
25379
|
+
api_duration_ms REAL,
|
|
25380
|
+
lines_added INTEGER,
|
|
25381
|
+
lines_removed INTEGER,
|
|
25382
|
+
files_changed TEXT NOT NULL DEFAULT '[]',
|
|
25383
|
+
commit_sha TEXT,
|
|
25384
|
+
attributes TEXT NOT NULL DEFAULT '{}'
|
|
25385
|
+
);
|
|
25386
|
+
|
|
25387
|
+
INSERT OR IGNORE INTO agent_events_dedup SELECT * FROM agent_events;
|
|
25388
|
+
|
|
25389
|
+
DROP TABLE agent_events;
|
|
25390
|
+
ALTER TABLE agent_events_dedup RENAME TO agent_events;
|
|
25391
|
+
|
|
25392
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON agent_events(session_id);
|
|
25393
|
+
CREATE INDEX IF NOT EXISTS idx_events_timestamp ON agent_events(event_timestamp);
|
|
25394
|
+
`);
|
|
25395
|
+
const afterCount = this.db.prepare("SELECT COUNT(*) as c FROM agent_events").get();
|
|
25396
|
+
console.log(`[agent-trace] migration complete: ${afterCount.c} events after dedup (removed ${total - afterCount.c} duplicates)`);
|
|
25397
|
+
this.rebuildSessionTracesFromEvents();
|
|
25398
|
+
}
|
|
25399
|
+
/**
|
|
25400
|
+
* Migration v2: fix databases that ran v0.2.6's broken rebuild (models_used='[]' with data present).
|
|
25401
|
+
* Re-runs the rebuild if session_traces exist but have empty models_used while events have model data.
|
|
25402
|
+
*/
|
|
25403
|
+
migrateRebuildBrokenTraces() {
|
|
25404
|
+
const tracesExist = this.db.prepare(
|
|
25405
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
|
|
25406
|
+
).get();
|
|
25407
|
+
if (tracesExist === void 0) {
|
|
25408
|
+
return;
|
|
25409
|
+
}
|
|
25410
|
+
const broken = this.db.prepare(`
|
|
25411
|
+
SELECT 1 FROM session_traces st
|
|
25412
|
+
WHERE st.models_used = '[]'
|
|
25413
|
+
AND EXISTS (
|
|
25414
|
+
SELECT 1 FROM agent_events ae
|
|
25415
|
+
WHERE ae.session_id = st.session_id AND ae.model IS NOT NULL
|
|
25416
|
+
)
|
|
25417
|
+
LIMIT 1
|
|
25418
|
+
`).get();
|
|
25419
|
+
if (broken === void 0) {
|
|
25420
|
+
return;
|
|
25421
|
+
}
|
|
25422
|
+
console.log("[agent-trace] migrating: rebuilding session traces with correct models/tools");
|
|
25423
|
+
this.rebuildSessionTracesFromEvents();
|
|
25424
|
+
}
|
|
25425
|
+
/**
|
|
25426
|
+
* Rebuild session_traces by aggregating deduplicated agent_events.
|
|
25427
|
+
* Called after dedup migration so the dashboard has correct metrics immediately.
|
|
25428
|
+
*/
|
|
25429
|
+
rebuildSessionTracesFromEvents() {
|
|
25430
|
+
const tracesExist = this.db.prepare(
|
|
25431
|
+
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='session_traces'"
|
|
25432
|
+
).get();
|
|
25433
|
+
if (tracesExist === void 0) {
|
|
25434
|
+
return;
|
|
25435
|
+
}
|
|
25436
|
+
this.db.exec("DELETE FROM session_traces");
|
|
25437
|
+
this.db.exec(`
|
|
25438
|
+
INSERT OR REPLACE INTO session_traces
|
|
25439
|
+
(session_id, version, started_at, ended_at, user_id, git_repo, git_branch,
|
|
25440
|
+
prompt_count, tool_call_count, api_call_count, total_cost_usd,
|
|
25441
|
+
total_input_tokens, total_output_tokens, lines_added, lines_removed,
|
|
25442
|
+
models_used, tools_used, files_touched, commit_count, updated_at)
|
|
25443
|
+
SELECT
|
|
25444
|
+
session_id,
|
|
25445
|
+
1,
|
|
25446
|
+
MIN(event_timestamp),
|
|
25447
|
+
MAX(event_timestamp),
|
|
25448
|
+
COALESCE(MAX(CASE WHEN user_id != 'unknown_user' THEN user_id END), 'unknown_user'),
|
|
25449
|
+
NULL,
|
|
25450
|
+
NULL,
|
|
25451
|
+
SUM(CASE WHEN event_type LIKE '%prompt%' THEN 1 ELSE 0 END),
|
|
25452
|
+
SUM(CASE WHEN event_type LIKE '%tool%' THEN 1 ELSE 0 END),
|
|
25453
|
+
SUM(CASE WHEN event_type LIKE '%api%' THEN 1 ELSE 0 END),
|
|
25454
|
+
COALESCE(SUM(cost_usd), 0),
|
|
25455
|
+
COALESCE(SUM(input_tokens), 0),
|
|
25456
|
+
COALESCE(SUM(output_tokens), 0),
|
|
25457
|
+
COALESCE(SUM(lines_added), 0),
|
|
25458
|
+
COALESCE(SUM(lines_removed), 0),
|
|
25459
|
+
'[]',
|
|
25460
|
+
'[]',
|
|
25461
|
+
'[]',
|
|
25462
|
+
COUNT(DISTINCT commit_sha),
|
|
25463
|
+
MAX(event_timestamp)
|
|
25464
|
+
FROM agent_events
|
|
25465
|
+
GROUP BY session_id
|
|
25466
|
+
`);
|
|
25467
|
+
const sessionIds = this.db.prepare(
|
|
25468
|
+
"SELECT DISTINCT session_id FROM agent_events"
|
|
25469
|
+
).all();
|
|
25470
|
+
const updateArrays = this.db.prepare(
|
|
25471
|
+
"UPDATE session_traces SET models_used = ?, tools_used = ?, files_touched = ? WHERE session_id = ?"
|
|
25472
|
+
);
|
|
25473
|
+
const transaction = this.db.transaction((ids) => {
|
|
25474
|
+
for (const { session_id } of ids) {
|
|
25475
|
+
const models = this.db.prepare(
|
|
25476
|
+
"SELECT DISTINCT model FROM agent_events WHERE session_id = ? AND model IS NOT NULL"
|
|
25477
|
+
).all(session_id);
|
|
25478
|
+
const tools = this.db.prepare(
|
|
25479
|
+
"SELECT DISTINCT tool_name FROM agent_events WHERE session_id = ? AND tool_name IS NOT NULL"
|
|
25480
|
+
).all(session_id);
|
|
25481
|
+
const files = this.db.prepare(
|
|
25482
|
+
"SELECT DISTINCT commit_sha FROM agent_events WHERE session_id = ? AND commit_sha IS NOT NULL"
|
|
25483
|
+
).all(session_id);
|
|
25484
|
+
updateArrays.run(
|
|
25485
|
+
JSON.stringify(models.map((r) => r.model)),
|
|
25486
|
+
JSON.stringify(tools.map((r) => r.tool_name)),
|
|
25487
|
+
JSON.stringify(files.map((r) => r.commit_sha)),
|
|
25488
|
+
session_id
|
|
25489
|
+
);
|
|
25490
|
+
}
|
|
25491
|
+
});
|
|
25492
|
+
transaction(sessionIds);
|
|
25493
|
+
const rebuilt = this.db.prepare("SELECT COUNT(*) as c FROM session_traces").get();
|
|
25494
|
+
console.log(`[agent-trace] rebuilt ${rebuilt.c} session traces from deduplicated events`);
|
|
25495
|
+
}
|
|
25322
25496
|
insertEvents(rows) {
|
|
25323
25497
|
const insert = this.db.prepare(`
|
|
25324
|
-
INSERT INTO agent_events
|
|
25498
|
+
INSERT OR IGNORE INTO agent_events
|
|
25325
25499
|
(event_id, event_type, event_timestamp, session_id, prompt_id, user_id, source, agent_type,
|
|
25326
25500
|
tool_name, tool_success, tool_duration_ms, model, cost_usd, input_tokens, output_tokens,
|
|
25327
25501
|
api_duration_ms, lines_added, lines_removed, files_changed, commit_sha, attributes)
|
|
@@ -28368,11 +28542,23 @@ async function close2(server) {
|
|
|
28368
28542
|
});
|
|
28369
28543
|
});
|
|
28370
28544
|
}
|
|
28545
|
+
function enrichEnvelopeWithCost(event) {
|
|
28546
|
+
const payload = event.payload;
|
|
28547
|
+
if (typeof payload["cost_usd"] === "number" && payload["cost_usd"] > 0) {
|
|
28548
|
+
return event;
|
|
28549
|
+
}
|
|
28550
|
+
const cost = computeEventCost(payload);
|
|
28551
|
+
if (cost === void 0 || cost <= 0) {
|
|
28552
|
+
return event;
|
|
28553
|
+
}
|
|
28554
|
+
return { ...event, payload: { ...payload, cost_usd: cost } };
|
|
28555
|
+
}
|
|
28371
28556
|
async function projectAndPersistEvent(event, sessionRepository, persistence) {
|
|
28372
28557
|
const current = sessionRepository.getBySessionId(event.sessionId);
|
|
28373
28558
|
const projected = projectEnvelopeToTrace(current, event);
|
|
28374
28559
|
sessionRepository.upsert(projected);
|
|
28375
|
-
|
|
28560
|
+
const enriched = enrichEnvelopeWithCost(event);
|
|
28561
|
+
await persistence.persistAcceptedEvent(enriched, projected);
|
|
28376
28562
|
}
|
|
28377
28563
|
function createRuntimeOtelSink(runtime) {
|
|
28378
28564
|
return runtime.collectorService.otelSink;
|