@roberttlange/agentlens 0.2.3 → 0.3.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/browser.js +154 -20
- package/dist/browser.js.map +1 -1
- package/dist/main.test.js +138 -1
- package/dist/main.test.js.map +1 -1
- package/node_modules/@agentlens/contracts/dist/index.d.ts +109 -0
- package/node_modules/@agentlens/core/dist/__tests__/config.test.js +18 -0
- package/node_modules/@agentlens/core/dist/__tests__/config.test.js.map +1 -1
- package/node_modules/@agentlens/core/dist/__tests__/index.test.js +370 -2
- package/node_modules/@agentlens/core/dist/__tests__/index.test.js.map +1 -1
- package/node_modules/@agentlens/core/dist/config.js +33 -0
- package/node_modules/@agentlens/core/dist/config.js.map +1 -1
- package/node_modules/@agentlens/core/dist/metrics.d.ts +13 -0
- package/node_modules/@agentlens/core/dist/metrics.js +98 -2
- package/node_modules/@agentlens/core/dist/metrics.js.map +1 -1
- package/node_modules/@agentlens/core/dist/sourceProfiles.js +4 -0
- package/node_modules/@agentlens/core/dist/sourceProfiles.js.map +1 -1
- package/node_modules/@agentlens/core/dist/traceIndex.d.ts +22 -1
- package/node_modules/@agentlens/core/dist/traceIndex.js +322 -21
- package/node_modules/@agentlens/core/dist/traceIndex.js.map +1 -1
- package/node_modules/@agentlens/server/dist/activity-cache.d.ts +6 -3
- package/node_modules/@agentlens/server/dist/activity-cache.js +6 -0
- package/node_modules/@agentlens/server/dist/activity-cache.js.map +1 -1
- package/node_modules/@agentlens/server/dist/activity-cache.test.js +86 -0
- package/node_modules/@agentlens/server/dist/activity-cache.test.js.map +1 -1
- package/node_modules/@agentlens/server/dist/activity.d.ts +20 -1
- package/node_modules/@agentlens/server/dist/activity.js +482 -6
- package/node_modules/@agentlens/server/dist/activity.js.map +1 -1
- package/node_modules/@agentlens/server/dist/app.d.ts +4 -2
- package/node_modules/@agentlens/server/dist/app.js +242 -5
- package/node_modules/@agentlens/server/dist/app.js.map +1 -1
- package/node_modules/@agentlens/server/dist/app.test.js +669 -8
- package/node_modules/@agentlens/server/dist/app.test.js.map +1 -1
- package/node_modules/@agentlens/server/dist/web/assets/index-CTFOBaBt.css +1 -0
- package/node_modules/@agentlens/server/dist/web/assets/index-CVf00w06.js +52 -0
- package/node_modules/@agentlens/server/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/node_modules/@agentlens/server/dist/web/assets/index-DjwZvHl6.css +0 -1
- package/node_modules/@agentlens/server/dist/web/assets/index-Ei_qfgA9.js +0 -52
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { mergeConfig, saveConfig, TraceIndex } from "@agentlens/core";
|
|
6
|
-
import { createServer, extractCursorSessionIdsFromOpenPaths, extractOpenCodeSessionIdsFromLogContent, geminiLogsContainSessionId, geminiProjectHashFromTracePath, geminiProjectHashesFromCwd, geminiProjectKeyFromTracePath, geminiProjectSlugsFromCwd, inferSessionCwd, matchesCurrentUser, parseTmuxClients, resolveDefaultWebDistPath, extractClaudeDebugProcessPid, selectAgentProcessPidsBySessionId, selectPreferredTmuxClient, selectOpenFileProcessPids, selectAgentProjectProcessPids, selectClaudeProjectProcessPids, selectCursorProjectProcessPids, selectGeminiProjectProcessPids, selectPidGroupByNearestTimestamp, } from "./app.js";
|
|
6
|
+
import { buildLiveBatchEnvelope, createServer, extractCursorSessionIdsFromOpenPaths, extractOpenCodeSessionIdsFromLogContent, geminiLogsContainSessionId, geminiProjectHashFromTracePath, geminiProjectHashesFromCwd, geminiProjectKeyFromTracePath, geminiProjectSlugsFromCwd, inferSessionCwd, matchesCurrentUser, parseTmuxClients, resolveDefaultWebDistPath, extractClaudeDebugProcessPid, selectAgentProcessPidsBySessionId, selectPreferredTmuxClient, selectOpenFileProcessPids, selectAgentProjectProcessPids, selectClaudeProjectProcessPids, selectCursorProjectProcessPids, selectGeminiProjectProcessPids, selectPidGroupByNearestTimestamp, runServer, } from "./app.js";
|
|
7
7
|
function buildTraceLog(sessionId, sequence, withToolEvents) {
|
|
8
8
|
const firstTs = new Date(Date.UTC(2026, 1, 11, 10, 0, sequence)).toISOString();
|
|
9
9
|
const secondTs = new Date(Date.UTC(2026, 1, 11, 10, 0, sequence + 1)).toISOString();
|
|
@@ -198,6 +198,94 @@ async function buildFixtureWithCustomTrace(traceLog, sessionId) {
|
|
|
198
198
|
return { configPath, index, sessionId };
|
|
199
199
|
}
|
|
200
200
|
describe("server api", () => {
|
|
201
|
+
it("coalesces bursty live deltas into one ordered batch envelope", async () => {
|
|
202
|
+
const original = buildTraceLog("server-session-burst", 1, true);
|
|
203
|
+
const updated = buildTraceLog("server-session-burst", 4, true);
|
|
204
|
+
const fixture = await buildFixtureWithCustomTrace(original, "server-session-burst");
|
|
205
|
+
const [initialSummary] = fixture.index.getSummaries();
|
|
206
|
+
if (!initialSummary)
|
|
207
|
+
throw new Error("missing initial summary");
|
|
208
|
+
await writeFile(path.join(path.dirname(initialSummary.path), path.basename(initialSummary.path)), `${original}\n${updated}`, "utf8");
|
|
209
|
+
await fixture.index.refresh();
|
|
210
|
+
const [updatedSummary] = fixture.index.getSummaries();
|
|
211
|
+
if (!updatedSummary)
|
|
212
|
+
throw new Error("missing updated summary");
|
|
213
|
+
const batch = buildLiveBatchEnvelope([
|
|
214
|
+
{
|
|
215
|
+
id: "1",
|
|
216
|
+
type: "trace_updated",
|
|
217
|
+
version: 1,
|
|
218
|
+
payload: { summary: { ...initialSummary, eventCount: 3, mtimeMs: initialSummary.mtimeMs + 1 } },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "2",
|
|
222
|
+
type: "trace_updated",
|
|
223
|
+
version: 2,
|
|
224
|
+
payload: { summary: updatedSummary },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "3",
|
|
228
|
+
type: "events_appended",
|
|
229
|
+
version: 3,
|
|
230
|
+
payload: {
|
|
231
|
+
id: updatedSummary.id,
|
|
232
|
+
appended: 1,
|
|
233
|
+
latestEvents: fixture.index.getTracePage(updatedSummary.id, { limit: 1 }).events,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "4",
|
|
238
|
+
type: "events_appended",
|
|
239
|
+
version: 4,
|
|
240
|
+
payload: {
|
|
241
|
+
id: updatedSummary.id,
|
|
242
|
+
appended: 2,
|
|
243
|
+
latestEvents: fixture.index.getTracePage(updatedSummary.id, { limit: 2 }).events,
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "5",
|
|
248
|
+
type: "overview_updated",
|
|
249
|
+
version: 5,
|
|
250
|
+
payload: { overview: fixture.index.getOverview(), startup: fixture.index.getStartupStatus() },
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: "6",
|
|
254
|
+
type: "trace_removed",
|
|
255
|
+
version: 6,
|
|
256
|
+
payload: { id: updatedSummary.id },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "7",
|
|
260
|
+
type: "trace_updated",
|
|
261
|
+
version: 7,
|
|
262
|
+
payload: { summary: { ...updatedSummary, mtimeMs: updatedSummary.mtimeMs + 5 } },
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
expect(batch.type).toBe("batch");
|
|
266
|
+
expect(batch.payload.events.map((event) => event.type)).toEqual(["trace_updated", "events_appended", "overview_updated"]);
|
|
267
|
+
const finalTraceUpdate = batch.payload.events[0];
|
|
268
|
+
expect(finalTraceUpdate?.type).toBe("trace_updated");
|
|
269
|
+
if (finalTraceUpdate?.type !== "trace_updated") {
|
|
270
|
+
throw new Error("expected trace_updated envelope");
|
|
271
|
+
}
|
|
272
|
+
expect(finalTraceUpdate.payload.summary).toMatchObject({
|
|
273
|
+
id: updatedSummary.id,
|
|
274
|
+
eventCount: updatedSummary.eventCount,
|
|
275
|
+
mtimeMs: updatedSummary.mtimeMs + 5,
|
|
276
|
+
});
|
|
277
|
+
const appended = batch.payload.events[1];
|
|
278
|
+
expect(appended?.type).toBe("events_appended");
|
|
279
|
+
if (appended?.type !== "events_appended") {
|
|
280
|
+
throw new Error("expected events_appended envelope");
|
|
281
|
+
}
|
|
282
|
+
expect(appended.payload).toMatchObject({
|
|
283
|
+
id: updatedSummary.id,
|
|
284
|
+
appended: 3,
|
|
285
|
+
});
|
|
286
|
+
expect(appended.payload.latestEvents).toHaveLength(2);
|
|
287
|
+
expect(batch.payload.events[2]?.type).toBe("overview_updated");
|
|
288
|
+
});
|
|
201
289
|
it("infers session cwd from both session_meta and session events", async () => {
|
|
202
290
|
const codexDetail = {
|
|
203
291
|
events: [{ rawType: "session_meta", raw: { payload: { cwd: " /tmp/codex " } } }],
|
|
@@ -868,8 +956,262 @@ describe("server api", () => {
|
|
|
868
956
|
expect(payload.activity.days[0]?.bins[1]?.startMs).toBe(Date.UTC(2026, 1, 5, 6, 30, 0));
|
|
869
957
|
await server.close();
|
|
870
958
|
}, 20_000);
|
|
871
|
-
it("
|
|
959
|
+
it("accepts metric and color overrides from activity query params", async () => {
|
|
960
|
+
const fixture = await buildFixtureWithTraceCount(1);
|
|
961
|
+
const server = await createServer({
|
|
962
|
+
traceIndex: fixture.index,
|
|
963
|
+
configPath: fixture.configPath,
|
|
964
|
+
enableStatic: false,
|
|
965
|
+
});
|
|
966
|
+
const weekResponse = await server.inject({
|
|
967
|
+
method: "GET",
|
|
968
|
+
url: "/api/activity/week?end_date=2026-02-11&tz_offset_min=0&day_count=7&slot_min=30&hour_start=6&hour_end=2&metric=output_tokens&color=%2316a34a",
|
|
969
|
+
});
|
|
970
|
+
expect(weekResponse.statusCode).toBe(200);
|
|
971
|
+
const weekPayload = weekResponse.json();
|
|
972
|
+
expect(weekPayload.activity.presentation).toMatchObject({
|
|
973
|
+
metric: "output_tokens",
|
|
974
|
+
color: "#16a34a",
|
|
975
|
+
});
|
|
976
|
+
const yearResponse = await server.inject({
|
|
977
|
+
method: "GET",
|
|
978
|
+
url: "/api/activity/year?end_date=2026-02-11&tz_offset_min=0&day_count=365&metric=bogus&color=red",
|
|
979
|
+
});
|
|
980
|
+
expect(yearResponse.statusCode).toBe(200);
|
|
981
|
+
const yearPayload = yearResponse.json();
|
|
982
|
+
expect(yearPayload.activity.presentation).toMatchObject({
|
|
983
|
+
metric: "sessions",
|
|
984
|
+
color: "#dc2626",
|
|
985
|
+
});
|
|
986
|
+
await server.close();
|
|
987
|
+
}, 20_000);
|
|
988
|
+
it("serves output-token heatmap presentation and summed weekly values from config", async () => {
|
|
989
|
+
const traceLog = [
|
|
990
|
+
JSON.stringify({
|
|
991
|
+
timestamp: "2026-02-11T06:00:00.000Z",
|
|
992
|
+
type: "session_meta",
|
|
993
|
+
payload: { id: "metric-trace", cwd: "/tmp/proj", cli_version: "0.1.0" },
|
|
994
|
+
}),
|
|
995
|
+
JSON.stringify({
|
|
996
|
+
timestamp: "2026-02-11T06:00:00.000Z",
|
|
997
|
+
type: "turn_context",
|
|
998
|
+
payload: { model: "gpt-5.3-codex" },
|
|
999
|
+
}),
|
|
1000
|
+
JSON.stringify({
|
|
1001
|
+
timestamp: "2026-02-11T06:00:00.000Z",
|
|
1002
|
+
type: "event_msg",
|
|
1003
|
+
payload: {
|
|
1004
|
+
type: "token_count",
|
|
1005
|
+
info: {
|
|
1006
|
+
total_token_usage: {
|
|
1007
|
+
input_tokens: 100,
|
|
1008
|
+
cached_input_tokens: 0,
|
|
1009
|
+
output_tokens: 60,
|
|
1010
|
+
reasoning_output_tokens: 0,
|
|
1011
|
+
total_tokens: 160,
|
|
1012
|
+
},
|
|
1013
|
+
last_token_usage: {
|
|
1014
|
+
input_tokens: 100,
|
|
1015
|
+
cached_input_tokens: 0,
|
|
1016
|
+
output_tokens: 60,
|
|
1017
|
+
reasoning_output_tokens: 0,
|
|
1018
|
+
total_tokens: 160,
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
}),
|
|
1023
|
+
JSON.stringify({
|
|
1024
|
+
timestamp: "2026-02-11T06:30:00.000Z",
|
|
1025
|
+
type: "response_item",
|
|
1026
|
+
payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "halfway" }] },
|
|
1027
|
+
}),
|
|
1028
|
+
JSON.stringify({
|
|
1029
|
+
timestamp: "2026-02-11T07:00:00.000Z",
|
|
1030
|
+
type: "response_item",
|
|
1031
|
+
payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "done" }] },
|
|
1032
|
+
}),
|
|
1033
|
+
].join("\n");
|
|
1034
|
+
const fixture = await buildFixtureWithCustomTrace(traceLog, "metric-trace");
|
|
1035
|
+
const nextConfig = mergeConfig({
|
|
1036
|
+
...fixture.index.getConfig(),
|
|
1037
|
+
activityHeatmap: {
|
|
1038
|
+
metric: "output_tokens",
|
|
1039
|
+
color: "#16a34a",
|
|
1040
|
+
},
|
|
1041
|
+
});
|
|
1042
|
+
fixture.index.setConfig(nextConfig);
|
|
1043
|
+
await saveConfig(nextConfig, fixture.configPath);
|
|
1044
|
+
const server = await createServer({
|
|
1045
|
+
traceIndex: fixture.index,
|
|
1046
|
+
configPath: fixture.configPath,
|
|
1047
|
+
enableStatic: false,
|
|
1048
|
+
});
|
|
1049
|
+
const response = await server.inject({
|
|
1050
|
+
method: "GET",
|
|
1051
|
+
url: "/api/activity/week?end_date=2026-02-11&tz_offset_min=0&day_count=1&slot_min=30&hour_start=6&hour_end=8",
|
|
1052
|
+
});
|
|
1053
|
+
expect(response.statusCode).toBe(200);
|
|
1054
|
+
const payload = response.json();
|
|
1055
|
+
const targetDay = payload.activity.days[0];
|
|
1056
|
+
expect(payload.activity.presentation).toMatchObject({
|
|
1057
|
+
metric: "output_tokens",
|
|
1058
|
+
color: "#16a34a",
|
|
1059
|
+
});
|
|
1060
|
+
expect(payload.activity.presentation.palette).toHaveLength(5);
|
|
1061
|
+
expect(targetDay?.bins[0]?.heatmapValue).toBeCloseTo(60, 6);
|
|
1062
|
+
expect(targetDay?.bins[1]?.heatmapValue).toBeCloseTo(0, 6);
|
|
1063
|
+
expect(targetDay?.bins[2]?.heatmapValue).toBeCloseTo(0, 6);
|
|
1064
|
+
await server.close();
|
|
1065
|
+
}, 20_000);
|
|
1066
|
+
it("builds weekly usage summaries from usage timestamps within the selected window", async () => {
|
|
1067
|
+
const traceLog = [
|
|
1068
|
+
JSON.stringify({
|
|
1069
|
+
timestamp: "2026-02-11T06:00:00.000Z",
|
|
1070
|
+
type: "session_meta",
|
|
1071
|
+
payload: { id: "weekly-usage-window", cwd: "/tmp/proj", cli_version: "0.1.0" },
|
|
1072
|
+
}),
|
|
1073
|
+
JSON.stringify({
|
|
1074
|
+
timestamp: "2026-02-11T06:00:00.000Z",
|
|
1075
|
+
type: "turn_context",
|
|
1076
|
+
payload: { model: "gpt-5.3-codex" },
|
|
1077
|
+
}),
|
|
1078
|
+
JSON.stringify({
|
|
1079
|
+
timestamp: "2026-02-11T06:05:00.000Z",
|
|
1080
|
+
type: "response_item",
|
|
1081
|
+
payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "within window" }] },
|
|
1082
|
+
}),
|
|
1083
|
+
JSON.stringify({
|
|
1084
|
+
timestamp: "2026-02-11T09:00:00.000Z",
|
|
1085
|
+
type: "event_msg",
|
|
1086
|
+
payload: {
|
|
1087
|
+
type: "token_count",
|
|
1088
|
+
info: {
|
|
1089
|
+
total_token_usage: {
|
|
1090
|
+
input_tokens: 120,
|
|
1091
|
+
cached_input_tokens: 30,
|
|
1092
|
+
output_tokens: 50,
|
|
1093
|
+
reasoning_output_tokens: 0,
|
|
1094
|
+
total_tokens: 200,
|
|
1095
|
+
},
|
|
1096
|
+
last_token_usage: {
|
|
1097
|
+
input_tokens: 120,
|
|
1098
|
+
cached_input_tokens: 30,
|
|
1099
|
+
output_tokens: 50,
|
|
1100
|
+
reasoning_output_tokens: 0,
|
|
1101
|
+
total_tokens: 200,
|
|
1102
|
+
},
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
}),
|
|
1106
|
+
JSON.stringify({
|
|
1107
|
+
timestamp: "2026-02-12T06:00:00.000Z",
|
|
1108
|
+
type: "response_item",
|
|
1109
|
+
payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "next day" }] },
|
|
1110
|
+
}),
|
|
1111
|
+
].join("\n");
|
|
1112
|
+
const fixture = await buildFixtureWithCustomTrace(traceLog, "weekly-usage-window");
|
|
1113
|
+
const server = await createServer({
|
|
1114
|
+
traceIndex: fixture.index,
|
|
1115
|
+
configPath: fixture.configPath,
|
|
1116
|
+
enableStatic: false,
|
|
1117
|
+
});
|
|
1118
|
+
const response = await server.inject({
|
|
1119
|
+
method: "GET",
|
|
1120
|
+
url: "/api/activity/week?end_date=2026-02-12&tz_offset_min=0&day_count=2&slot_min=30&hour_start=6&hour_end=8",
|
|
1121
|
+
});
|
|
1122
|
+
expect(response.statusCode).toBe(200);
|
|
1123
|
+
const payload = response.json();
|
|
1124
|
+
const codexRow = payload.activity.usageSummary.rows.find((row) => row.agent === "codex");
|
|
1125
|
+
expect(codexRow).toEqual(expect.objectContaining({
|
|
1126
|
+
inputTokens: 0,
|
|
1127
|
+
cacheTokens: 0,
|
|
1128
|
+
outputTokens: 0,
|
|
1129
|
+
}));
|
|
1130
|
+
await server.close();
|
|
1131
|
+
}, 20_000);
|
|
1132
|
+
it("serves compact yearly activity from the earliest active day in the selected year", async () => {
|
|
872
1133
|
const fixture = await buildFixtureWithTraceCount(3);
|
|
1134
|
+
const detailSpy = vi.spyOn(fixture.index, "getSessionDetail");
|
|
1135
|
+
const server = await createServer({
|
|
1136
|
+
traceIndex: fixture.index,
|
|
1137
|
+
configPath: fixture.configPath,
|
|
1138
|
+
enableStatic: false,
|
|
1139
|
+
});
|
|
1140
|
+
const response = await server.inject({
|
|
1141
|
+
method: "GET",
|
|
1142
|
+
url: "/api/activity/year?end_date=2026-02-22&tz_offset_min=0&day_count=53",
|
|
1143
|
+
});
|
|
1144
|
+
expect(response.statusCode).toBe(200);
|
|
1145
|
+
const payload = response.json();
|
|
1146
|
+
expect(payload.activity.startDateLocal).toBe("2026-02-11");
|
|
1147
|
+
expect(payload.activity.endDateLocal).toBe("2026-02-22");
|
|
1148
|
+
expect(payload.activity.dayCount).toBe(12);
|
|
1149
|
+
expect(payload.activity.days).toHaveLength(12);
|
|
1150
|
+
expect(payload.activity.days[0]?.dateLocal).toBe("2026-02-11");
|
|
1151
|
+
expect(payload.activity.days[11]?.dateLocal).toBe("2026-02-22");
|
|
1152
|
+
expect(payload.activity.days.every((day) => day.bins === undefined)).toBe(true);
|
|
1153
|
+
expect(payload.activity.days.some((day) => day.totalEventCount > 0)).toBe(true);
|
|
1154
|
+
expect(payload.activity.usageSummary.totals.totalUniqueSessions).toBe(3);
|
|
1155
|
+
expect(detailSpy).not.toHaveBeenCalled();
|
|
1156
|
+
detailSpy.mockRestore();
|
|
1157
|
+
await server.close();
|
|
1158
|
+
}, 20_000);
|
|
1159
|
+
it("serves cost heatmap presentation and summed yearly values from config", async () => {
|
|
1160
|
+
const traceLog = [
|
|
1161
|
+
JSON.stringify({
|
|
1162
|
+
timestamp: "2026-02-11T07:00:00.000Z",
|
|
1163
|
+
type: "session_meta",
|
|
1164
|
+
payload: { id: "cost-trace", cwd: "/tmp/proj", cli_version: "0.1.0" },
|
|
1165
|
+
}),
|
|
1166
|
+
JSON.stringify({
|
|
1167
|
+
timestamp: "2026-02-11T07:00:00.000Z",
|
|
1168
|
+
type: "turn_context",
|
|
1169
|
+
payload: { model: "gpt-5.3-codex" },
|
|
1170
|
+
}),
|
|
1171
|
+
JSON.stringify({
|
|
1172
|
+
timestamp: "2026-02-11T07:00:00.000Z",
|
|
1173
|
+
type: "event_msg",
|
|
1174
|
+
payload: {
|
|
1175
|
+
type: "token_count",
|
|
1176
|
+
info: {
|
|
1177
|
+
total_token_usage: {
|
|
1178
|
+
input_tokens: 40,
|
|
1179
|
+
cached_input_tokens: 0,
|
|
1180
|
+
output_tokens: 60,
|
|
1181
|
+
reasoning_output_tokens: 0,
|
|
1182
|
+
total_tokens: 100,
|
|
1183
|
+
},
|
|
1184
|
+
last_token_usage: {
|
|
1185
|
+
input_tokens: 40,
|
|
1186
|
+
cached_input_tokens: 0,
|
|
1187
|
+
output_tokens: 60,
|
|
1188
|
+
reasoning_output_tokens: 0,
|
|
1189
|
+
total_tokens: 100,
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
}),
|
|
1194
|
+
JSON.stringify({
|
|
1195
|
+
timestamp: "2026-02-11T19:00:00.000Z",
|
|
1196
|
+
type: "response_item",
|
|
1197
|
+
payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "day one" }] },
|
|
1198
|
+
}),
|
|
1199
|
+
JSON.stringify({
|
|
1200
|
+
timestamp: "2026-02-12T19:00:00.000Z",
|
|
1201
|
+
type: "response_item",
|
|
1202
|
+
payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "day two" }] },
|
|
1203
|
+
}),
|
|
1204
|
+
].join("\n");
|
|
1205
|
+
const fixture = await buildFixtureWithCustomTrace(traceLog, "cost-trace");
|
|
1206
|
+
const nextConfig = mergeConfig({
|
|
1207
|
+
...fixture.index.getConfig(),
|
|
1208
|
+
activityHeatmap: {
|
|
1209
|
+
metric: "total_cost_usd",
|
|
1210
|
+
color: "#2563eb",
|
|
1211
|
+
},
|
|
1212
|
+
});
|
|
1213
|
+
fixture.index.setConfig(nextConfig);
|
|
1214
|
+
await saveConfig(nextConfig, fixture.configPath);
|
|
873
1215
|
const server = await createServer({
|
|
874
1216
|
traceIndex: fixture.index,
|
|
875
1217
|
configPath: fixture.configPath,
|
|
@@ -877,17 +1219,103 @@ describe("server api", () => {
|
|
|
877
1219
|
});
|
|
878
1220
|
const response = await server.inject({
|
|
879
1221
|
method: "GET",
|
|
880
|
-
url: "/api/activity/
|
|
1222
|
+
url: "/api/activity/year?end_date=2026-02-12&tz_offset_min=0&day_count=365",
|
|
881
1223
|
});
|
|
882
1224
|
expect(response.statusCode).toBe(200);
|
|
883
1225
|
const payload = response.json();
|
|
884
|
-
expect(payload.activity.
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1226
|
+
expect(payload.activity.presentation).toMatchObject({
|
|
1227
|
+
metric: "total_cost_usd",
|
|
1228
|
+
color: "#2563eb",
|
|
1229
|
+
});
|
|
1230
|
+
const firstDayCost = payload.activity.days[0]?.heatmapValue ?? 0;
|
|
1231
|
+
const secondDayCost = payload.activity.days[1]?.heatmapValue ?? 0;
|
|
1232
|
+
expect(firstDayCost).toBeCloseTo(0.0001, 6);
|
|
1233
|
+
expect(secondDayCost).toBeCloseTo(0, 6);
|
|
889
1234
|
await server.close();
|
|
890
1235
|
}, 20_000);
|
|
1236
|
+
it("builds yearly activity without reparsing cold traces", async () => {
|
|
1237
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "agentlens-server-year-cold-"));
|
|
1238
|
+
const codexRoot = path.join(root, ".codex", "sessions", "2026", "02", "11");
|
|
1239
|
+
await mkdir(codexRoot, { recursive: true });
|
|
1240
|
+
for (const [index, filename] of ["rollout-a.jsonl", "rollout-b.jsonl", "rollout-c.jsonl"].entries()) {
|
|
1241
|
+
await writeFile(path.join(codexRoot, filename), [
|
|
1242
|
+
JSON.stringify({
|
|
1243
|
+
timestamp: `2026-02-11T10:00:0${index}.000Z`,
|
|
1244
|
+
type: "session_meta",
|
|
1245
|
+
payload: { id: `server-cold-${index}`, cwd: "/tmp/project", cli_version: "0.1.0" },
|
|
1246
|
+
}),
|
|
1247
|
+
JSON.stringify({
|
|
1248
|
+
timestamp: `2026-02-11T10:05:0${index}.000Z`,
|
|
1249
|
+
type: "response_item",
|
|
1250
|
+
payload: {
|
|
1251
|
+
type: "message",
|
|
1252
|
+
role: "assistant",
|
|
1253
|
+
content: [{ type: "output_text", text: `trace-${index}` }],
|
|
1254
|
+
},
|
|
1255
|
+
}),
|
|
1256
|
+
].join("\n") + "\n", "utf8");
|
|
1257
|
+
}
|
|
1258
|
+
const config = mergeConfig({
|
|
1259
|
+
retention: {
|
|
1260
|
+
strategy: "aggressive_recency",
|
|
1261
|
+
hotTraceCount: 1,
|
|
1262
|
+
warmTraceCount: 0,
|
|
1263
|
+
maxResidentEventsPerHotTrace: 1000,
|
|
1264
|
+
maxResidentEventsPerWarmTrace: 50,
|
|
1265
|
+
detailLoadMode: "lazy_from_disk",
|
|
1266
|
+
},
|
|
1267
|
+
sessionLogDirectories: [],
|
|
1268
|
+
sources: {
|
|
1269
|
+
codex_home: {
|
|
1270
|
+
name: "codex_home",
|
|
1271
|
+
enabled: true,
|
|
1272
|
+
roots: [path.join(root, ".codex", "sessions")],
|
|
1273
|
+
includeGlobs: ["**/*.jsonl"],
|
|
1274
|
+
excludeGlobs: [],
|
|
1275
|
+
maxDepth: 8,
|
|
1276
|
+
agentHint: "codex",
|
|
1277
|
+
},
|
|
1278
|
+
claude_projects: {
|
|
1279
|
+
name: "claude_projects",
|
|
1280
|
+
enabled: false,
|
|
1281
|
+
roots: [],
|
|
1282
|
+
includeGlobs: ["**/*.jsonl"],
|
|
1283
|
+
excludeGlobs: [],
|
|
1284
|
+
maxDepth: 8,
|
|
1285
|
+
agentHint: "claude",
|
|
1286
|
+
},
|
|
1287
|
+
claude_history: {
|
|
1288
|
+
name: "claude_history",
|
|
1289
|
+
enabled: false,
|
|
1290
|
+
roots: [],
|
|
1291
|
+
includeGlobs: ["history.jsonl"],
|
|
1292
|
+
excludeGlobs: [],
|
|
1293
|
+
maxDepth: 8,
|
|
1294
|
+
agentHint: "claude",
|
|
1295
|
+
},
|
|
1296
|
+
},
|
|
1297
|
+
});
|
|
1298
|
+
const configPath = path.join(root, "config.toml");
|
|
1299
|
+
await saveConfig(config, configPath);
|
|
1300
|
+
const index = new TraceIndex(config);
|
|
1301
|
+
await index.start();
|
|
1302
|
+
expect(index.getSummaries()[2]?.residentTier).not.toBe("hot");
|
|
1303
|
+
const internal = index;
|
|
1304
|
+
const parseFileSyncSpy = vi.spyOn(internal.parserRegistry, "parseFileSync");
|
|
1305
|
+
const server = await createServer({
|
|
1306
|
+
traceIndex: index,
|
|
1307
|
+
configPath,
|
|
1308
|
+
enableStatic: false,
|
|
1309
|
+
});
|
|
1310
|
+
const response = await server.inject({
|
|
1311
|
+
method: "GET",
|
|
1312
|
+
url: "/api/activity/year?end_date=2026-02-11&tz_offset_min=0&day_count=365",
|
|
1313
|
+
});
|
|
1314
|
+
expect(response.statusCode).toBe(200);
|
|
1315
|
+
expect(parseFileSyncSpy).not.toHaveBeenCalled();
|
|
1316
|
+
await server.close();
|
|
1317
|
+
index.stop();
|
|
1318
|
+
}, 20_000);
|
|
891
1319
|
it("returns validation errors for invalid activity day query params", async () => {
|
|
892
1320
|
const fixture = await buildFixtureWithTraceCount(1);
|
|
893
1321
|
const server = await createServer({
|
|
@@ -1002,6 +1430,239 @@ describe("server api", () => {
|
|
|
1002
1430
|
expect(byStart.get(Date.UTC(2026, 1, 11, 13, 0, 0))).toBe(1);
|
|
1003
1431
|
await server.close();
|
|
1004
1432
|
}, 20_000);
|
|
1433
|
+
it("listens before trace index startup resolves", async () => {
|
|
1434
|
+
const fixture = await buildFixture();
|
|
1435
|
+
let releaseStart = () => { };
|
|
1436
|
+
const startDeferred = new Promise((resolve) => {
|
|
1437
|
+
releaseStart = resolve;
|
|
1438
|
+
});
|
|
1439
|
+
let startupState = {
|
|
1440
|
+
phase: "bootstrapping",
|
|
1441
|
+
inspectorReady: false,
|
|
1442
|
+
fullReady: false,
|
|
1443
|
+
isPartial: false,
|
|
1444
|
+
discoveredTraceCount: 0,
|
|
1445
|
+
hydratedTraceCount: 0,
|
|
1446
|
+
startupError: "",
|
|
1447
|
+
};
|
|
1448
|
+
const startSpy = vi.spyOn(fixture.index, "start").mockImplementation(async () => {
|
|
1449
|
+
await startDeferred;
|
|
1450
|
+
startupState = {
|
|
1451
|
+
phase: "ready",
|
|
1452
|
+
inspectorReady: true,
|
|
1453
|
+
fullReady: true,
|
|
1454
|
+
isPartial: false,
|
|
1455
|
+
discoveredTraceCount: 1,
|
|
1456
|
+
hydratedTraceCount: 1,
|
|
1457
|
+
startupError: "",
|
|
1458
|
+
};
|
|
1459
|
+
});
|
|
1460
|
+
const startupSpy = vi.spyOn(fixture.index, "getStartupStatus").mockImplementation(() => startupState);
|
|
1461
|
+
const server = await runServer({
|
|
1462
|
+
host: "127.0.0.1",
|
|
1463
|
+
port: 0,
|
|
1464
|
+
configPath: fixture.configPath,
|
|
1465
|
+
enableStatic: false,
|
|
1466
|
+
traceIndex: fixture.index,
|
|
1467
|
+
});
|
|
1468
|
+
try {
|
|
1469
|
+
expect(startSpy).toHaveBeenCalledOnce();
|
|
1470
|
+
const health = await server.inject({ method: "GET", url: "/api/healthz" });
|
|
1471
|
+
expect(health.statusCode).toBe(200);
|
|
1472
|
+
expect(health.json()).toEqual({ ok: true });
|
|
1473
|
+
const notReady = await server.inject({ method: "GET", url: "/api/readyz" });
|
|
1474
|
+
expect(notReady.statusCode).toBe(503);
|
|
1475
|
+
expect(notReady.json()).toEqual({ ok: false, ready: false, startup: startupState });
|
|
1476
|
+
releaseStart();
|
|
1477
|
+
await startDeferred;
|
|
1478
|
+
const ready = await server.inject({ method: "GET", url: "/api/readyz" });
|
|
1479
|
+
expect(ready.statusCode).toBe(200);
|
|
1480
|
+
expect(ready.json()).toEqual({ ok: true, ready: true, startup: startupState });
|
|
1481
|
+
}
|
|
1482
|
+
finally {
|
|
1483
|
+
fixture.index.stop();
|
|
1484
|
+
await server.close();
|
|
1485
|
+
startSpy.mockRestore();
|
|
1486
|
+
startupSpy.mockRestore();
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
it("surfaces trace index startup failures through readiness endpoint", async () => {
|
|
1490
|
+
const fixture = await buildFixture();
|
|
1491
|
+
const startSpy = vi.spyOn(fixture.index, "start").mockRejectedValue(new Error("boom on startup"));
|
|
1492
|
+
const startupState = {
|
|
1493
|
+
phase: "failed",
|
|
1494
|
+
inspectorReady: false,
|
|
1495
|
+
fullReady: false,
|
|
1496
|
+
isPartial: false,
|
|
1497
|
+
discoveredTraceCount: 0,
|
|
1498
|
+
hydratedTraceCount: 0,
|
|
1499
|
+
startupError: "boom on startup",
|
|
1500
|
+
};
|
|
1501
|
+
const startupSpy = vi.spyOn(fixture.index, "getStartupStatus").mockReturnValue(startupState);
|
|
1502
|
+
const server = await runServer({
|
|
1503
|
+
host: "127.0.0.1",
|
|
1504
|
+
port: 0,
|
|
1505
|
+
configPath: fixture.configPath,
|
|
1506
|
+
enableStatic: false,
|
|
1507
|
+
traceIndex: fixture.index,
|
|
1508
|
+
});
|
|
1509
|
+
try {
|
|
1510
|
+
expect(startSpy).toHaveBeenCalledOnce();
|
|
1511
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1512
|
+
const ready = await server.inject({ method: "GET", url: "/api/readyz" });
|
|
1513
|
+
expect(ready.statusCode).toBe(503);
|
|
1514
|
+
expect(ready.json()).toEqual({
|
|
1515
|
+
ok: false,
|
|
1516
|
+
ready: false,
|
|
1517
|
+
startup: startupState,
|
|
1518
|
+
startupError: "boom on startup",
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
finally {
|
|
1522
|
+
fixture.index.stop();
|
|
1523
|
+
await server.close();
|
|
1524
|
+
startSpy.mockRestore();
|
|
1525
|
+
startupSpy.mockRestore();
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
it("serves partial overview and warming activity while background hydration continues", async () => {
|
|
1529
|
+
const fixture = await buildFixture();
|
|
1530
|
+
const startupState = {
|
|
1531
|
+
phase: "hydrating",
|
|
1532
|
+
inspectorReady: true,
|
|
1533
|
+
fullReady: false,
|
|
1534
|
+
isPartial: true,
|
|
1535
|
+
discoveredTraceCount: 240,
|
|
1536
|
+
hydratedTraceCount: 120,
|
|
1537
|
+
startupError: "",
|
|
1538
|
+
};
|
|
1539
|
+
const startupSpy = vi.spyOn(fixture.index, "getStartupStatus").mockReturnValue(startupState);
|
|
1540
|
+
const dayProgress = {
|
|
1541
|
+
ready: false,
|
|
1542
|
+
relevantDiscoveredCount: 240,
|
|
1543
|
+
relevantHydratedCount: 120,
|
|
1544
|
+
percent: 50,
|
|
1545
|
+
};
|
|
1546
|
+
const progressSpy = vi.spyOn(fixture.index, "getHydrationProgress").mockReturnValue(dayProgress);
|
|
1547
|
+
const server = await createServer({
|
|
1548
|
+
traceIndex: fixture.index,
|
|
1549
|
+
configPath: fixture.configPath,
|
|
1550
|
+
enableStatic: false,
|
|
1551
|
+
});
|
|
1552
|
+
try {
|
|
1553
|
+
const ready = await server.inject({ method: "GET", url: "/api/readyz" });
|
|
1554
|
+
expect(ready.statusCode).toBe(200);
|
|
1555
|
+
expect(ready.json()).toEqual({ ok: true, ready: true, startup: startupState });
|
|
1556
|
+
const overview = await server.inject({ method: "GET", url: "/api/overview" });
|
|
1557
|
+
expect(overview.statusCode).toBe(200);
|
|
1558
|
+
expect(overview.json()).toMatchObject({
|
|
1559
|
+
overview: expect.any(Object),
|
|
1560
|
+
startup: startupState,
|
|
1561
|
+
});
|
|
1562
|
+
const day = await server.inject({
|
|
1563
|
+
method: "GET",
|
|
1564
|
+
url: "/api/activity/day?date=2026-02-11&tz_offset_min=0&bin_min=5&break_min=10",
|
|
1565
|
+
});
|
|
1566
|
+
expect(day.statusCode).toBe(503);
|
|
1567
|
+
expect(day.json()).toEqual({
|
|
1568
|
+
warming: true,
|
|
1569
|
+
progress: dayProgress,
|
|
1570
|
+
});
|
|
1571
|
+
const year = await server.inject({
|
|
1572
|
+
method: "GET",
|
|
1573
|
+
url: "/api/activity/year?end_date=2026-02-11&tz_offset_min=0&day_count=365",
|
|
1574
|
+
});
|
|
1575
|
+
expect(year.statusCode).toBe(503);
|
|
1576
|
+
expect(year.json()).toEqual({
|
|
1577
|
+
warming: true,
|
|
1578
|
+
progress: dayProgress,
|
|
1579
|
+
});
|
|
1580
|
+
}
|
|
1581
|
+
finally {
|
|
1582
|
+
progressSpy.mockRestore();
|
|
1583
|
+
startupSpy.mockRestore();
|
|
1584
|
+
fixture.index.stop();
|
|
1585
|
+
await server.close();
|
|
1586
|
+
}
|
|
1587
|
+
});
|
|
1588
|
+
it("keeps activity warming until inspector bootstrap metadata is ready", async () => {
|
|
1589
|
+
const fixture = await buildFixture();
|
|
1590
|
+
const startupState = {
|
|
1591
|
+
phase: "bootstrapping",
|
|
1592
|
+
inspectorReady: false,
|
|
1593
|
+
fullReady: false,
|
|
1594
|
+
isPartial: false,
|
|
1595
|
+
discoveredTraceCount: 0,
|
|
1596
|
+
hydratedTraceCount: 0,
|
|
1597
|
+
startupError: "",
|
|
1598
|
+
};
|
|
1599
|
+
const startupSpy = vi.spyOn(fixture.index, "getStartupStatus").mockReturnValue(startupState);
|
|
1600
|
+
const progressSpy = vi.spyOn(fixture.index, "getHydrationProgress").mockReturnValue({
|
|
1601
|
+
ready: true,
|
|
1602
|
+
relevantDiscoveredCount: 12,
|
|
1603
|
+
relevantHydratedCount: 12,
|
|
1604
|
+
percent: 100,
|
|
1605
|
+
});
|
|
1606
|
+
const server = await createServer({
|
|
1607
|
+
traceIndex: fixture.index,
|
|
1608
|
+
configPath: fixture.configPath,
|
|
1609
|
+
enableStatic: false,
|
|
1610
|
+
});
|
|
1611
|
+
try {
|
|
1612
|
+
const day = await server.inject({
|
|
1613
|
+
method: "GET",
|
|
1614
|
+
url: "/api/activity/day?date=2026-02-11&tz_offset_min=0&bin_min=5&break_min=10",
|
|
1615
|
+
});
|
|
1616
|
+
expect(day.statusCode).toBe(503);
|
|
1617
|
+
expect(day.json()).toEqual({
|
|
1618
|
+
warming: true,
|
|
1619
|
+
progress: {
|
|
1620
|
+
ready: false,
|
|
1621
|
+
relevantDiscoveredCount: 0,
|
|
1622
|
+
relevantHydratedCount: 0,
|
|
1623
|
+
percent: 0,
|
|
1624
|
+
},
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
finally {
|
|
1628
|
+
progressSpy.mockRestore();
|
|
1629
|
+
startupSpy.mockRestore();
|
|
1630
|
+
fixture.index.stop();
|
|
1631
|
+
await server.close();
|
|
1632
|
+
}
|
|
1633
|
+
});
|
|
1634
|
+
it("allows day activity to load before slower year hydration completes", async () => {
|
|
1635
|
+
const fixture = await buildFixture();
|
|
1636
|
+
const progressSpy = vi.spyOn(fixture.index, "getHydrationProgress").mockImplementation((windowStartMs) => windowStartMs >= Date.UTC(2026, 1, 11, 7, 0, 0)
|
|
1637
|
+
? { ready: true, relevantDiscoveredCount: 12, relevantHydratedCount: 12, percent: 100 }
|
|
1638
|
+
: { ready: false, relevantDiscoveredCount: 240, relevantHydratedCount: 120, percent: 50 });
|
|
1639
|
+
const server = await createServer({
|
|
1640
|
+
traceIndex: fixture.index,
|
|
1641
|
+
configPath: fixture.configPath,
|
|
1642
|
+
enableStatic: false,
|
|
1643
|
+
});
|
|
1644
|
+
try {
|
|
1645
|
+
const day = await server.inject({
|
|
1646
|
+
method: "GET",
|
|
1647
|
+
url: "/api/activity/day?date=2026-02-11&tz_offset_min=0&bin_min=5&break_min=10",
|
|
1648
|
+
});
|
|
1649
|
+
expect(day.statusCode).toBe(200);
|
|
1650
|
+
const year = await server.inject({
|
|
1651
|
+
method: "GET",
|
|
1652
|
+
url: "/api/activity/year?end_date=2026-02-11&tz_offset_min=0&day_count=365",
|
|
1653
|
+
});
|
|
1654
|
+
expect(year.statusCode).toBe(503);
|
|
1655
|
+
expect(year.json()).toEqual({
|
|
1656
|
+
warming: true,
|
|
1657
|
+
progress: { ready: false, relevantDiscoveredCount: 240, relevantHydratedCount: 120, percent: 50 },
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
finally {
|
|
1661
|
+
progressSpy.mockRestore();
|
|
1662
|
+
fixture.index.stop();
|
|
1663
|
+
await server.close();
|
|
1664
|
+
}
|
|
1665
|
+
});
|
|
1005
1666
|
it("serves overview, trace listing, trace details, stop/open controls, and config updates", async () => {
|
|
1006
1667
|
const fixture = await buildFixture();
|
|
1007
1668
|
const stopTraceSession = vi.fn();
|