agent-profiler 1.0.0 → 1.0.1
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/README.md +33 -3
- package/assets/dashboard.png +0 -0
- package/dist/adapters/cursor.js +5 -1
- package/dist/cli.js +23 -0
- package/dist/commands/dashboard.js +17 -0
- package/dist/commands/init.js +26 -0
- package/dist/commands/last.js +3 -191
- package/dist/core/dashboardServer.js +294 -0
- package/dist/core/db.js +164 -54
- package/dist/core/eventMetadata.js +7 -2
- package/dist/core/gitWorkspace.js +16 -4
- package/dist/core/sessionAnalytics.js +204 -0
- package/dist/dashboard/app.js +329 -0
- package/dist/dashboard/index.html +89 -0
- package/dist/dashboard/styles.css +389 -0
- package/package.json +5 -3
package/dist/core/db.js
CHANGED
|
@@ -70,7 +70,10 @@ function migrateTableColumns(db, tableName, columnsToAdd) {
|
|
|
70
70
|
}
|
|
71
71
|
function migrateEventsSchema(db) {
|
|
72
72
|
migrateTableColumns(db, "events", [
|
|
73
|
-
{
|
|
73
|
+
{
|
|
74
|
+
name: "workspace_path",
|
|
75
|
+
sql: `ALTER TABLE events ADD COLUMN workspace_path TEXT`,
|
|
76
|
+
},
|
|
74
77
|
{
|
|
75
78
|
name: "workspace_home_rel_path",
|
|
76
79
|
sql: `ALTER TABLE events ADD COLUMN workspace_home_rel_path TEXT`,
|
|
@@ -79,7 +82,10 @@ function migrateEventsSchema(db) {
|
|
|
79
82
|
name: "workspace_display_path",
|
|
80
83
|
sql: `ALTER TABLE events ADD COLUMN workspace_display_path TEXT`,
|
|
81
84
|
},
|
|
82
|
-
{
|
|
85
|
+
{
|
|
86
|
+
name: "git_repo_root",
|
|
87
|
+
sql: `ALTER TABLE events ADD COLUMN git_repo_root TEXT`,
|
|
88
|
+
},
|
|
83
89
|
{
|
|
84
90
|
name: "git_repo_root_home_rel_path",
|
|
85
91
|
sql: `ALTER TABLE events ADD COLUMN git_repo_root_home_rel_path TEXT`,
|
|
@@ -88,18 +94,30 @@ function migrateEventsSchema(db) {
|
|
|
88
94
|
name: "git_repo_root_display_path",
|
|
89
95
|
sql: `ALTER TABLE events ADD COLUMN git_repo_root_display_path TEXT`,
|
|
90
96
|
},
|
|
91
|
-
{
|
|
92
|
-
|
|
97
|
+
{
|
|
98
|
+
name: "git_repo_name",
|
|
99
|
+
sql: `ALTER TABLE events ADD COLUMN git_repo_name TEXT`,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "git_branch",
|
|
103
|
+
sql: `ALTER TABLE events ADD COLUMN git_branch TEXT`,
|
|
104
|
+
},
|
|
93
105
|
{
|
|
94
106
|
name: "interaction_kind",
|
|
95
107
|
sql: `ALTER TABLE events ADD COLUMN interaction_kind TEXT`,
|
|
96
108
|
},
|
|
97
|
-
{
|
|
109
|
+
{
|
|
110
|
+
name: "correlation_id",
|
|
111
|
+
sql: `ALTER TABLE events ADD COLUMN correlation_id TEXT`,
|
|
112
|
+
},
|
|
98
113
|
{
|
|
99
114
|
name: "tool_canonical_name",
|
|
100
115
|
sql: `ALTER TABLE events ADD COLUMN tool_canonical_name TEXT`,
|
|
101
116
|
},
|
|
102
|
-
{
|
|
117
|
+
{
|
|
118
|
+
name: "mcp_server",
|
|
119
|
+
sql: `ALTER TABLE events ADD COLUMN mcp_server TEXT`,
|
|
120
|
+
},
|
|
103
121
|
{ name: "mcp_tool", sql: `ALTER TABLE events ADD COLUMN mcp_tool TEXT` },
|
|
104
122
|
{
|
|
105
123
|
name: "payload_byte_length",
|
|
@@ -113,13 +131,22 @@ function migrateEventsSchema(db) {
|
|
|
113
131
|
}
|
|
114
132
|
function migrateInteractionSpansSchema(db) {
|
|
115
133
|
migrateTableColumns(db, "interaction_spans", [
|
|
116
|
-
{
|
|
134
|
+
{
|
|
135
|
+
name: "turn_id",
|
|
136
|
+
sql: `ALTER TABLE interaction_spans ADD COLUMN turn_id TEXT`,
|
|
137
|
+
},
|
|
117
138
|
{
|
|
118
139
|
name: "tool_canonical_name",
|
|
119
140
|
sql: `ALTER TABLE interaction_spans ADD COLUMN tool_canonical_name TEXT`,
|
|
120
141
|
},
|
|
121
|
-
{
|
|
122
|
-
|
|
142
|
+
{
|
|
143
|
+
name: "mcp_server",
|
|
144
|
+
sql: `ALTER TABLE interaction_spans ADD COLUMN mcp_server TEXT`,
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "mcp_tool",
|
|
148
|
+
sql: `ALTER TABLE interaction_spans ADD COLUMN mcp_tool TEXT`,
|
|
149
|
+
},
|
|
123
150
|
{
|
|
124
151
|
name: "pre_event_id",
|
|
125
152
|
sql: `ALTER TABLE interaction_spans ADD COLUMN pre_event_id INTEGER`,
|
|
@@ -168,9 +195,18 @@ function migrateInteractionSpansSchema(db) {
|
|
|
168
195
|
name: "git_repo_name",
|
|
169
196
|
sql: `ALTER TABLE interaction_spans ADD COLUMN git_repo_name TEXT`,
|
|
170
197
|
},
|
|
171
|
-
{
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
{
|
|
199
|
+
name: "git_branch",
|
|
200
|
+
sql: `ALTER TABLE interaction_spans ADD COLUMN git_branch TEXT`,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: "started_at",
|
|
204
|
+
sql: `ALTER TABLE interaction_spans ADD COLUMN started_at TEXT`,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: "completed_at",
|
|
208
|
+
sql: `ALTER TABLE interaction_spans ADD COLUMN completed_at TEXT`,
|
|
209
|
+
},
|
|
174
210
|
]);
|
|
175
211
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_events_workspace ON events(workspace_path, created_at)`);
|
|
176
212
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_events_interaction_kind ON events(interaction_kind, created_at)`);
|
|
@@ -351,27 +387,7 @@ export function getEventsForLatestSession(db) {
|
|
|
351
387
|
? db
|
|
352
388
|
.prepare(`
|
|
353
389
|
SELECT
|
|
354
|
-
|
|
355
|
-
created_at AS createdAt,
|
|
356
|
-
source,
|
|
357
|
-
source_event AS sourceEvent,
|
|
358
|
-
repo_path AS repoPath,
|
|
359
|
-
session_id AS sessionId,
|
|
360
|
-
turn_id AS turnId,
|
|
361
|
-
model,
|
|
362
|
-
role,
|
|
363
|
-
estimated_input_tokens AS estimatedInputTokens,
|
|
364
|
-
estimated_output_tokens AS estimatedOutputTokens,
|
|
365
|
-
estimated_total_tokens AS estimatedTotalTokens,
|
|
366
|
-
raw_payload AS rawPayload,
|
|
367
|
-
workspace_path AS workspacePath,
|
|
368
|
-
workspace_home_rel_path AS workspaceHomeRelPath,
|
|
369
|
-
workspace_display_path AS workspaceDisplayPath,
|
|
370
|
-
git_repo_root AS gitRepoRoot,
|
|
371
|
-
git_repo_root_home_rel_path AS gitRepoRootHomeRelPath,
|
|
372
|
-
git_repo_root_display_path AS gitRepoRootDisplayPath,
|
|
373
|
-
git_repo_name AS gitRepoName,
|
|
374
|
-
git_branch AS gitBranch
|
|
390
|
+
${STORED_EVENT_SELECT}
|
|
375
391
|
FROM events
|
|
376
392
|
WHERE source = ? AND session_id = ?
|
|
377
393
|
ORDER BY created_at ASC, id ASC
|
|
@@ -380,27 +396,7 @@ export function getEventsForLatestSession(db) {
|
|
|
380
396
|
: db
|
|
381
397
|
.prepare(`
|
|
382
398
|
SELECT
|
|
383
|
-
|
|
384
|
-
created_at AS createdAt,
|
|
385
|
-
source,
|
|
386
|
-
source_event AS sourceEvent,
|
|
387
|
-
repo_path AS repoPath,
|
|
388
|
-
session_id AS sessionId,
|
|
389
|
-
turn_id AS turnId,
|
|
390
|
-
model,
|
|
391
|
-
role,
|
|
392
|
-
estimated_input_tokens AS estimatedInputTokens,
|
|
393
|
-
estimated_output_tokens AS estimatedOutputTokens,
|
|
394
|
-
estimated_total_tokens AS estimatedTotalTokens,
|
|
395
|
-
raw_payload AS rawPayload,
|
|
396
|
-
workspace_path AS workspacePath,
|
|
397
|
-
workspace_home_rel_path AS workspaceHomeRelPath,
|
|
398
|
-
workspace_display_path AS workspaceDisplayPath,
|
|
399
|
-
git_repo_root AS gitRepoRoot,
|
|
400
|
-
git_repo_root_home_rel_path AS gitRepoRootHomeRelPath,
|
|
401
|
-
git_repo_root_display_path AS gitRepoRootDisplayPath,
|
|
402
|
-
git_repo_name AS gitRepoName,
|
|
403
|
-
git_branch AS gitBranch
|
|
399
|
+
${STORED_EVENT_SELECT}
|
|
404
400
|
FROM events
|
|
405
401
|
WHERE source = ? AND repo_path IS ?
|
|
406
402
|
ORDER BY created_at DESC, id DESC
|
|
@@ -410,3 +406,117 @@ export function getEventsForLatestSession(db) {
|
|
|
410
406
|
.reverse();
|
|
411
407
|
return rows;
|
|
412
408
|
}
|
|
409
|
+
const STORED_EVENT_SELECT = `
|
|
410
|
+
id,
|
|
411
|
+
created_at AS createdAt,
|
|
412
|
+
source,
|
|
413
|
+
source_event AS sourceEvent,
|
|
414
|
+
repo_path AS repoPath,
|
|
415
|
+
session_id AS sessionId,
|
|
416
|
+
turn_id AS turnId,
|
|
417
|
+
model,
|
|
418
|
+
role,
|
|
419
|
+
estimated_input_tokens AS estimatedInputTokens,
|
|
420
|
+
estimated_output_tokens AS estimatedOutputTokens,
|
|
421
|
+
estimated_total_tokens AS estimatedTotalTokens,
|
|
422
|
+
raw_payload AS rawPayload,
|
|
423
|
+
workspace_path AS workspacePath,
|
|
424
|
+
workspace_home_rel_path AS workspaceHomeRelPath,
|
|
425
|
+
workspace_display_path AS workspaceDisplayPath,
|
|
426
|
+
git_repo_root AS gitRepoRoot,
|
|
427
|
+
git_repo_root_home_rel_path AS gitRepoRootHomeRelPath,
|
|
428
|
+
git_repo_root_display_path AS gitRepoRootDisplayPath,
|
|
429
|
+
git_repo_name AS gitRepoName,
|
|
430
|
+
git_branch AS gitBranch
|
|
431
|
+
`;
|
|
432
|
+
export function getLatestSessionDescriptor(db) {
|
|
433
|
+
const latest = db
|
|
434
|
+
.prepare(`
|
|
435
|
+
SELECT source, session_id AS sessionId, repo_path AS repoPath
|
|
436
|
+
FROM events
|
|
437
|
+
ORDER BY created_at DESC
|
|
438
|
+
LIMIT 1
|
|
439
|
+
`)
|
|
440
|
+
.get();
|
|
441
|
+
return latest ?? null;
|
|
442
|
+
}
|
|
443
|
+
export function listRecentSessions(db, limit) {
|
|
444
|
+
const rows = db
|
|
445
|
+
.prepare(`
|
|
446
|
+
SELECT
|
|
447
|
+
source AS source,
|
|
448
|
+
session_id AS sessionId,
|
|
449
|
+
MAX(repo_path) AS repoPath,
|
|
450
|
+
MIN(created_at) AS startedAt,
|
|
451
|
+
MAX(created_at) AS endedAt,
|
|
452
|
+
COUNT(*) AS eventCount
|
|
453
|
+
FROM events
|
|
454
|
+
WHERE session_id IS NOT NULL AND LENGTH(TRIM(session_id)) > 0
|
|
455
|
+
GROUP BY source, session_id
|
|
456
|
+
ORDER BY endedAt DESC
|
|
457
|
+
LIMIT ?
|
|
458
|
+
`)
|
|
459
|
+
.all(limit);
|
|
460
|
+
return rows;
|
|
461
|
+
}
|
|
462
|
+
export function getEventsForSession(db, source, sessionId) {
|
|
463
|
+
const rows = db
|
|
464
|
+
.prepare(`
|
|
465
|
+
SELECT
|
|
466
|
+
${STORED_EVENT_SELECT}
|
|
467
|
+
FROM events
|
|
468
|
+
WHERE source = ? AND session_id = ?
|
|
469
|
+
ORDER BY created_at ASC, id ASC
|
|
470
|
+
`)
|
|
471
|
+
.all(source, sessionId);
|
|
472
|
+
return rows;
|
|
473
|
+
}
|
|
474
|
+
/** Latest window used when session_id is absent (matches getEventsForLatestSession fallback). */
|
|
475
|
+
export function getEventsForLegacyRepoWindow(db, source, repoPath) {
|
|
476
|
+
const rows = db
|
|
477
|
+
.prepare(`
|
|
478
|
+
SELECT
|
|
479
|
+
${STORED_EVENT_SELECT}
|
|
480
|
+
FROM events
|
|
481
|
+
WHERE source = ? AND repo_path IS ?
|
|
482
|
+
ORDER BY created_at DESC, id DESC
|
|
483
|
+
LIMIT 200
|
|
484
|
+
`)
|
|
485
|
+
.all(source, repoPath ?? null);
|
|
486
|
+
return rows.reverse();
|
|
487
|
+
}
|
|
488
|
+
export function getSessionTimeline(db, source, sessionId) {
|
|
489
|
+
const rows = db
|
|
490
|
+
.prepare(`
|
|
491
|
+
SELECT
|
|
492
|
+
id,
|
|
493
|
+
created_at AS createdAt,
|
|
494
|
+
role,
|
|
495
|
+
turn_id AS turnId,
|
|
496
|
+
estimated_total_tokens AS estimatedTotalTokens,
|
|
497
|
+
source_event AS sourceEvent
|
|
498
|
+
FROM events
|
|
499
|
+
WHERE source = ? AND session_id = ?
|
|
500
|
+
ORDER BY created_at ASC, id ASC
|
|
501
|
+
`)
|
|
502
|
+
.all(source, sessionId);
|
|
503
|
+
return rows;
|
|
504
|
+
}
|
|
505
|
+
export function getLegacyRepoTimeline(db, source, repoPath) {
|
|
506
|
+
const rows = db
|
|
507
|
+
.prepare(`
|
|
508
|
+
SELECT
|
|
509
|
+
id,
|
|
510
|
+
created_at AS createdAt,
|
|
511
|
+
role,
|
|
512
|
+
turn_id AS turnId,
|
|
513
|
+
estimated_total_tokens AS estimatedTotalTokens,
|
|
514
|
+
source_event AS sourceEvent
|
|
515
|
+
FROM events
|
|
516
|
+
WHERE source = ? AND repo_path IS ?
|
|
517
|
+
ORDER BY created_at DESC, id DESC
|
|
518
|
+
LIMIT 200
|
|
519
|
+
`)
|
|
520
|
+
.all(source, repoPath ?? null);
|
|
521
|
+
return rows.reverse();
|
|
522
|
+
}
|
|
@@ -21,7 +21,10 @@ export function parseMcpToolName(canonical) {
|
|
|
21
21
|
if (t.startsWith("mcp__")) {
|
|
22
22
|
const parts = t.split("__").filter(Boolean);
|
|
23
23
|
if (parts.length >= 3) {
|
|
24
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
mcpServer: parts[1] ?? null,
|
|
26
|
+
mcpTool: parts.slice(2).join("__") || null,
|
|
27
|
+
};
|
|
25
28
|
}
|
|
26
29
|
}
|
|
27
30
|
if (t.toLowerCase().startsWith("mcp:")) {
|
|
@@ -102,7 +105,9 @@ export function deriveIngestFields(source, hookEventName, rawPayload, rawJsonTex
|
|
|
102
105
|
const payload = asRecord(rawPayload);
|
|
103
106
|
const correlationId = extractCorrelationId(payload);
|
|
104
107
|
let toolCanonicalName = extractToolCanonicalName(payload);
|
|
105
|
-
if (!toolCanonicalName &&
|
|
108
|
+
if (!toolCanonicalName &&
|
|
109
|
+
payload.tool_input &&
|
|
110
|
+
typeof payload.tool_input === "object") {
|
|
106
111
|
const ti = payload.tool_input;
|
|
107
112
|
toolCanonicalName =
|
|
108
113
|
pickFirstString([ti.command, ti.tool, ti.name])?.trim() ||
|
|
@@ -58,7 +58,9 @@ export function resolveHookWorkspacePath(normalizedRepoPath, rawPayload) {
|
|
|
58
58
|
for (const c of candidates) {
|
|
59
59
|
if (!c)
|
|
60
60
|
continue;
|
|
61
|
-
return path.isAbsolute(c)
|
|
61
|
+
return path.isAbsolute(c)
|
|
62
|
+
? path.normalize(c)
|
|
63
|
+
: path.resolve(process.cwd(), c);
|
|
62
64
|
}
|
|
63
65
|
return path.resolve(process.cwd());
|
|
64
66
|
}
|
|
@@ -84,7 +86,10 @@ function gitOutput(workspacePath, args) {
|
|
|
84
86
|
export function resolveWorkspaceGitMeta(workspacePath) {
|
|
85
87
|
const normalizedWorkspacePath = path.normalize(workspacePath);
|
|
86
88
|
const workspaceHomePath = deriveHomePath(normalizedWorkspacePath);
|
|
87
|
-
const inside = gitOutput(normalizedWorkspacePath, [
|
|
89
|
+
const inside = gitOutput(normalizedWorkspacePath, [
|
|
90
|
+
"rev-parse",
|
|
91
|
+
"--is-inside-work-tree",
|
|
92
|
+
]);
|
|
88
93
|
if (inside !== "true") {
|
|
89
94
|
return {
|
|
90
95
|
workspacePath: normalizedWorkspacePath,
|
|
@@ -97,8 +102,15 @@ export function resolveWorkspaceGitMeta(workspacePath) {
|
|
|
97
102
|
gitBranch: null,
|
|
98
103
|
};
|
|
99
104
|
}
|
|
100
|
-
const root = gitOutput(normalizedWorkspacePath, [
|
|
101
|
-
|
|
105
|
+
const root = gitOutput(normalizedWorkspacePath, [
|
|
106
|
+
"rev-parse",
|
|
107
|
+
"--show-toplevel",
|
|
108
|
+
]);
|
|
109
|
+
const branch = gitOutput(normalizedWorkspacePath, [
|
|
110
|
+
"rev-parse",
|
|
111
|
+
"--abbrev-ref",
|
|
112
|
+
"HEAD",
|
|
113
|
+
]);
|
|
102
114
|
const gitRepoRoot = root ? path.normalize(root) : null;
|
|
103
115
|
const gitRepoRootHomePath = deriveHomePath(gitRepoRoot);
|
|
104
116
|
const gitRepoName = gitRepoRoot ? path.basename(gitRepoRoot) : null;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { runContextAudit } from "./contextAudit.js";
|
|
2
|
+
function parsePayload(rawPayload) {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = JSON.parse(rawPayload);
|
|
5
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
6
|
+
return parsed;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// ignore malformed payloads
|
|
11
|
+
}
|
|
12
|
+
return {};
|
|
13
|
+
}
|
|
14
|
+
export function formatTokens(value) {
|
|
15
|
+
return `~${new Intl.NumberFormat("en-US").format(value)} tokens`;
|
|
16
|
+
}
|
|
17
|
+
function normalizeSnippet(value) {
|
|
18
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 160);
|
|
19
|
+
}
|
|
20
|
+
export const LARGEST_EVENTS_LIMIT = 10;
|
|
21
|
+
function sessionKeyLabel(first) {
|
|
22
|
+
if (first.sessionId && first.sessionId.trim().length > 0) {
|
|
23
|
+
return first.sessionId;
|
|
24
|
+
}
|
|
25
|
+
return first.repoPath ?? "(no-session-id)";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Builds the same report shape as the CLI `last` command from ordered session events.
|
|
29
|
+
*/
|
|
30
|
+
export function analyzeSession(events, options) {
|
|
31
|
+
if (events.length === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const first = events[0];
|
|
35
|
+
const lastEv = events[events.length - 1];
|
|
36
|
+
const start = new Date(first.createdAt).getTime();
|
|
37
|
+
const end = new Date(lastEv.createdAt).getTime();
|
|
38
|
+
const durationMinutes = Math.max(0, Math.round((end - start) / 60000));
|
|
39
|
+
let input = 0;
|
|
40
|
+
let output = 0;
|
|
41
|
+
let total = 0;
|
|
42
|
+
let shellOutput = 0;
|
|
43
|
+
let toolResults = 0;
|
|
44
|
+
const turnIds = new Set();
|
|
45
|
+
let fileEdits = 0;
|
|
46
|
+
let shellCalls = 0;
|
|
47
|
+
let toolCalls = 0;
|
|
48
|
+
const fileEditCounts = new Map();
|
|
49
|
+
const redFlags = [];
|
|
50
|
+
let recommendations = [];
|
|
51
|
+
const shellFailureBuckets = new Map();
|
|
52
|
+
for (const event of events) {
|
|
53
|
+
input += event.estimatedInputTokens;
|
|
54
|
+
output += event.estimatedOutputTokens;
|
|
55
|
+
total += event.estimatedTotalTokens;
|
|
56
|
+
if (event.turnId)
|
|
57
|
+
turnIds.add(event.turnId);
|
|
58
|
+
if (event.role === "file_edit") {
|
|
59
|
+
fileEdits += 1;
|
|
60
|
+
const payload = parsePayload(event.rawPayload);
|
|
61
|
+
const file = (payload.filePath ??
|
|
62
|
+
payload.path ??
|
|
63
|
+
payload.relativePath);
|
|
64
|
+
if (file) {
|
|
65
|
+
fileEditCounts.set(file, (fileEditCounts.get(file) ?? 0) + 1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (event.role === "shell_command")
|
|
69
|
+
shellCalls += 1;
|
|
70
|
+
if (event.role === "shell_output") {
|
|
71
|
+
shellCalls += 1;
|
|
72
|
+
shellOutput += event.estimatedTotalTokens;
|
|
73
|
+
const payload = parsePayload(event.rawPayload);
|
|
74
|
+
const command = typeof payload.command === "string" ? payload.command : null;
|
|
75
|
+
const stderr = typeof payload.stderr === "string" ? payload.stderr : "";
|
|
76
|
+
const stdout = typeof payload.stdout === "string" ? payload.stdout : "";
|
|
77
|
+
const outputSnippet = normalizeSnippet(stderr || stdout);
|
|
78
|
+
const looksFailed = outputSnippet.includes("error") ||
|
|
79
|
+
outputSnippet.includes("failed") ||
|
|
80
|
+
outputSnippet.includes("exception") ||
|
|
81
|
+
outputSnippet.includes("traceback");
|
|
82
|
+
if (command && looksFailed) {
|
|
83
|
+
const key = `${normalizeSnippet(command)}|${outputSnippet}`;
|
|
84
|
+
const prev = shellFailureBuckets.get(key) ?? { runs: 0, tokenTotal: 0 };
|
|
85
|
+
shellFailureBuckets.set(key, {
|
|
86
|
+
runs: prev.runs + 1,
|
|
87
|
+
tokenTotal: prev.tokenTotal + event.estimatedTotalTokens,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (event.role === "tool_call")
|
|
92
|
+
toolCalls += 1;
|
|
93
|
+
if (event.role === "tool_result" || event.role === "tool_failure")
|
|
94
|
+
toolResults += event.estimatedTotalTokens;
|
|
95
|
+
}
|
|
96
|
+
let score = 100;
|
|
97
|
+
const topChurn = [...fileEditCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
98
|
+
if (topChurn && topChurn[1] >= 5) {
|
|
99
|
+
redFlags.push({
|
|
100
|
+
severity: "HIGH",
|
|
101
|
+
title: "same-file churn",
|
|
102
|
+
detail: `${topChurn[0]} was edited ${topChurn[1]} times.`,
|
|
103
|
+
recommendation: "Add a focused repo rule or skill note for this file's recurring failure pattern.",
|
|
104
|
+
penalty: 15,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (shellOutput > 4000) {
|
|
108
|
+
redFlags.push({
|
|
109
|
+
severity: "HIGH",
|
|
110
|
+
title: "shell output noise",
|
|
111
|
+
detail: `Shell output produced ${formatTokens(shellOutput)} in this session.`,
|
|
112
|
+
recommendation: "Capture key test/build failures once, then summarize repeated output.",
|
|
113
|
+
penalty: 12,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (toolResults > 4000) {
|
|
117
|
+
redFlags.push({
|
|
118
|
+
severity: "MEDIUM",
|
|
119
|
+
title: "large tool result",
|
|
120
|
+
detail: `Tool results produced ${formatTokens(toolResults)} in this session.`,
|
|
121
|
+
recommendation: "Request narrower tool queries and summarize oversized tool responses.",
|
|
122
|
+
penalty: 10,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const worstFailureLoop = [...shellFailureBuckets.entries()].sort((a, b) => b[1].runs - a[1].runs || b[1].tokenTotal - a[1].tokenTotal)[0];
|
|
126
|
+
if (worstFailureLoop && worstFailureLoop[1].runs >= 3) {
|
|
127
|
+
redFlags.push({
|
|
128
|
+
severity: "HIGH",
|
|
129
|
+
title: "thrashing loop",
|
|
130
|
+
detail: `A similar failing shell command looped ${worstFailureLoop[1].runs} times (${formatTokens(worstFailureLoop[1].tokenTotal)}).`,
|
|
131
|
+
recommendation: "Pause after repeated failures, capture one root error, then adjust strategy.",
|
|
132
|
+
penalty: 14,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const largestPrompt = events
|
|
136
|
+
.filter((e) => e.role === "user_prompt")
|
|
137
|
+
.reduce((max, cur) => Math.max(max, cur.estimatedTotalTokens), 0);
|
|
138
|
+
if (largestPrompt > 8000) {
|
|
139
|
+
redFlags.push({
|
|
140
|
+
severity: "MEDIUM",
|
|
141
|
+
title: "oversized prompt",
|
|
142
|
+
detail: `Largest prompt was ${formatTokens(largestPrompt)}.`,
|
|
143
|
+
recommendation: "Split goals into smaller requests and load reference docs on demand.",
|
|
144
|
+
penalty: 8,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (total > 12000 && fileEdits === 0) {
|
|
148
|
+
redFlags.push({
|
|
149
|
+
severity: "MEDIUM",
|
|
150
|
+
title: "low-signal session",
|
|
151
|
+
detail: `High observable usage (${formatTokens(total)}) with no file edits.`,
|
|
152
|
+
recommendation: "Push for earlier implementation checkpoints instead of extended analysis.",
|
|
153
|
+
penalty: 10,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const contextAudit = runContextAudit(options.contextAuditRoot);
|
|
157
|
+
if (contextAudit.totalEstimatedTokens > 6000) {
|
|
158
|
+
redFlags.push({
|
|
159
|
+
severity: "MEDIUM",
|
|
160
|
+
title: "context bloat",
|
|
161
|
+
detail: `Always-on instruction files estimate ${formatTokens(contextAudit.totalEstimatedTokens)}.`,
|
|
162
|
+
recommendation: "Move large static references into on-demand skills or targeted commands.",
|
|
163
|
+
penalty: 10,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
for (const flag of redFlags)
|
|
167
|
+
score -= flag.penalty;
|
|
168
|
+
score = Math.max(0, score);
|
|
169
|
+
recommendations = [...new Set(redFlags.map((r) => r.recommendation))];
|
|
170
|
+
if (recommendations.length === 0) {
|
|
171
|
+
recommendations = [
|
|
172
|
+
"No major waste pattern detected. Keep prompts scoped and continue tracking trends.",
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
const largestEvents = [...events]
|
|
176
|
+
.sort((a, b) => b.estimatedTotalTokens - a.estimatedTotalTokens)
|
|
177
|
+
.slice(0, LARGEST_EVENTS_LIMIT)
|
|
178
|
+
.map((e) => ({
|
|
179
|
+
role: e.role,
|
|
180
|
+
sourceEvent: e.sourceEvent,
|
|
181
|
+
estimatedTotalTokens: e.estimatedTotalTokens,
|
|
182
|
+
}));
|
|
183
|
+
return {
|
|
184
|
+
source: first.source,
|
|
185
|
+
sessionKey: sessionKeyLabel(first),
|
|
186
|
+
repo: first.repoPath ?? options.contextAuditRoot,
|
|
187
|
+
durationMinutes,
|
|
188
|
+
usage: { input, output, toolResults, shellOutput, total },
|
|
189
|
+
sessionShape: {
|
|
190
|
+
turns: turnIds.size,
|
|
191
|
+
fileEdits,
|
|
192
|
+
shellCalls,
|
|
193
|
+
toolCalls,
|
|
194
|
+
},
|
|
195
|
+
largestEvents,
|
|
196
|
+
efficiencyScore: score,
|
|
197
|
+
redFlags: redFlags.map((flag) => ({
|
|
198
|
+
severity: flag.severity,
|
|
199
|
+
title: flag.title,
|
|
200
|
+
detail: flag.detail,
|
|
201
|
+
})),
|
|
202
|
+
recommendations,
|
|
203
|
+
};
|
|
204
|
+
}
|