noctrace 1.0.0 → 1.2.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/README.md +2 -0
- package/bin/noctrace.js +43 -8
- package/dist/client/assets/index-C0dmCjnv.js +30 -0
- package/dist/client/assets/{index-DwPuae45.css → index-DYum71Dc.css} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/server/docker.js +65 -0
- package/dist/server/server/index.js +2 -0
- package/dist/server/server/rollup.js +336 -0
- package/dist/server/server/routes/patterns.js +42 -0
- package/dist/server/server/summary-cache.js +51 -0
- package/dist/server/shared/session-summary.js +123 -0
- package/package.json +5 -1
- package/dist/client/assets/index-D3XepZ5e.js +0 -30
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { computeContextHealth } from './health.js';
|
|
2
|
+
import { parseCompactionBoundaries } from './session-metadata.js';
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Helpers
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
/** Recursively collect all rows, including nested children, into a flat list. */
|
|
7
|
+
function flattenRows(rows) {
|
|
8
|
+
const result = [];
|
|
9
|
+
const walk = (list) => {
|
|
10
|
+
for (const row of list) {
|
|
11
|
+
result.push(row);
|
|
12
|
+
if (row.children.length > 0)
|
|
13
|
+
walk(row.children);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
walk(rows);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Public API
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
/**
|
|
23
|
+
* Build a {@link PatternSessionSummary} from a parsed WaterfallRow array.
|
|
24
|
+
*
|
|
25
|
+
* Tolerates empty sessions (returns zero counts, null grade, null model).
|
|
26
|
+
* Never throws.
|
|
27
|
+
*/
|
|
28
|
+
export function buildSessionSummary(rows, sessionId, projectSlug) {
|
|
29
|
+
const flat = flattenRows(rows);
|
|
30
|
+
// --- time bounds ---
|
|
31
|
+
let startMs = Infinity;
|
|
32
|
+
let endMs = -Infinity;
|
|
33
|
+
for (const row of flat) {
|
|
34
|
+
if (row.startTime < startMs)
|
|
35
|
+
startMs = row.startTime;
|
|
36
|
+
const end = row.endTime ?? row.startTime;
|
|
37
|
+
if (end > endMs)
|
|
38
|
+
endMs = end;
|
|
39
|
+
}
|
|
40
|
+
if (!isFinite(startMs))
|
|
41
|
+
startMs = 0;
|
|
42
|
+
if (!isFinite(endMs))
|
|
43
|
+
endMs = 0;
|
|
44
|
+
// --- primary model: model with most assistant turns ---
|
|
45
|
+
const modelTurnCounts = new Map();
|
|
46
|
+
for (const row of flat) {
|
|
47
|
+
if (row.modelName) {
|
|
48
|
+
modelTurnCounts.set(row.modelName, (modelTurnCounts.get(row.modelName) ?? 0) + 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let primaryModel = null;
|
|
52
|
+
let maxTurns = 0;
|
|
53
|
+
for (const [model, count] of modelTurnCounts) {
|
|
54
|
+
if (count > maxTurns) {
|
|
55
|
+
maxTurns = count;
|
|
56
|
+
primaryModel = model;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// --- health ---
|
|
60
|
+
let healthGrade = null;
|
|
61
|
+
let healthScore = null;
|
|
62
|
+
if (rows.length > 0) {
|
|
63
|
+
// We need compaction count. Derive from flat rows (compact_boundary rows produce
|
|
64
|
+
// tool rows with toolName 'compact_boundary' or we use a fallback of 0 since
|
|
65
|
+
// we don't have the raw JSONL string here). We count via health module directly.
|
|
66
|
+
const compactionCount = flat.filter((r) => r.type === 'tool' && r.toolName === 'compact_boundary').length;
|
|
67
|
+
const health = computeContextHealth(rows, compactionCount);
|
|
68
|
+
healthGrade = health.grade;
|
|
69
|
+
healthScore = health.score;
|
|
70
|
+
}
|
|
71
|
+
// --- tool stats (tool-type rows only, not agent/api-error/hook/turn) ---
|
|
72
|
+
const toolCounts = {};
|
|
73
|
+
const toolFailures = {};
|
|
74
|
+
const toolLatencies = {};
|
|
75
|
+
for (const row of flat) {
|
|
76
|
+
if (row.type !== 'tool')
|
|
77
|
+
continue;
|
|
78
|
+
const name = row.toolName;
|
|
79
|
+
toolCounts[name] = (toolCounts[name] ?? 0) + 1;
|
|
80
|
+
if (row.isFailure) {
|
|
81
|
+
toolFailures[name] = (toolFailures[name] ?? 0) + 1;
|
|
82
|
+
}
|
|
83
|
+
if (row.duration !== null) {
|
|
84
|
+
if (!toolLatencies[name])
|
|
85
|
+
toolLatencies[name] = [];
|
|
86
|
+
toolLatencies[name].push(row.duration);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// --- compaction count from compaction boundaries (agent rows tagged as compact) ---
|
|
90
|
+
// Since compact_boundary records produce system rows (not tool rows), and health.ts
|
|
91
|
+
// receives compactionCount from the caller (parseCompactionBoundaries), we re-derive it
|
|
92
|
+
// by counting rows whose toolName is the compact boundary sentinel used in the parser.
|
|
93
|
+
// As a fallback, inspect rows for any health score that already reflects compactions.
|
|
94
|
+
// The most reliable approach: count rows with type==='tool' && toolName==='compact_boundary'.
|
|
95
|
+
// The parser emits no such rows; compactions are system records counted separately.
|
|
96
|
+
// We set compactionCount=0 here and rely on the rollup caller to pass a better value
|
|
97
|
+
// when it has the raw content. However, for pure-row callers, we approximate by checking
|
|
98
|
+
// for compaction-indicative health signals.
|
|
99
|
+
const compactionCount = flat.filter((r) => r.type === 'tool' && r.toolName === 'compact_boundary').length;
|
|
100
|
+
return {
|
|
101
|
+
sessionId,
|
|
102
|
+
projectSlug,
|
|
103
|
+
startMs,
|
|
104
|
+
endMs,
|
|
105
|
+
primaryModel,
|
|
106
|
+
healthGrade,
|
|
107
|
+
healthScore,
|
|
108
|
+
toolCounts,
|
|
109
|
+
toolFailures,
|
|
110
|
+
toolLatencies,
|
|
111
|
+
compactionCount,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a PatternSessionSummary when the raw JSONL content string is available.
|
|
116
|
+
* This variant correctly counts compaction boundaries from the raw content.
|
|
117
|
+
*/
|
|
118
|
+
export function buildSessionSummaryFromContent(rows, sessionId, projectSlug, rawContent) {
|
|
119
|
+
const summary = buildSessionSummary(rows, sessionId, projectSlug);
|
|
120
|
+
// Override compactionCount with the accurate value from the raw JSONL
|
|
121
|
+
const boundaries = parseCompactionBoundaries(rawContent);
|
|
122
|
+
return { ...summary, compactionCount: boundaries.length };
|
|
123
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "noctrace",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Claude Code observability — DevTools-style waterfall visualizer for AI agent workflows, token tracking, and context health monitoring",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -72,6 +72,9 @@
|
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@tailwindcss/vite": "4.2.2",
|
|
75
|
+
"@testing-library/jest-dom": "6.6.3",
|
|
76
|
+
"@testing-library/react": "16.3.0",
|
|
77
|
+
"@testing-library/user-event": "14.6.1",
|
|
75
78
|
"@types/express": "5.0.6",
|
|
76
79
|
"@types/node": "22.19.15",
|
|
77
80
|
"@types/react": "19.2.14",
|
|
@@ -80,6 +83,7 @@
|
|
|
80
83
|
"@vitejs/plugin-react": "5.2.0",
|
|
81
84
|
"concurrently": "9.2.1",
|
|
82
85
|
"eslint": "9.39.4",
|
|
86
|
+
"happy-dom": "18.0.1",
|
|
83
87
|
"react": "19.2.4",
|
|
84
88
|
"react-dom": "19.2.4",
|
|
85
89
|
"tailwindcss": "4.2.2",
|