@newrelic/preflight 1.5.4 → 1.5.6
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/dashboard/routes/api-handler.d.ts +8 -1
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +163 -76
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/dashboard/subagent-timeline-store.d.ts +66 -0
- package/dist/dashboard/subagent-timeline-store.d.ts.map +1 -1
- package/dist/dashboard/subagent-timeline-store.js +193 -1
- package/dist/dashboard/subagent-timeline-store.js.map +1 -1
- package/dist/dashboard/workflow-store.d.ts +9 -0
- package/dist/dashboard/workflow-store.d.ts.map +1 -1
- package/dist/dashboard/workflow-store.js +24 -15
- package/dist/dashboard/workflow-store.js.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ import { type SessionLikeForCostOutcome } from '../../metrics/cost-per-outcome.j
|
|
|
5
5
|
import type { ToolCallRecord } from '../../storage/types.js';
|
|
6
6
|
import type { FullSessionSummary, SessionFileInfo } from '../../storage/session-store.js';
|
|
7
7
|
import type { WorkflowRunRow } from '../workflow-store.js';
|
|
8
|
-
import type { SubagentTimeline, AgentCall } from '../subagent-timeline-store.js';
|
|
8
|
+
import type { SubagentTimeline, AgentCall, LiveWorkflowRunDetail } from '../subagent-timeline-store.js';
|
|
9
9
|
import type { CostForecast } from '../../metrics/cost-forecast.js';
|
|
10
10
|
import type { AlertEvent } from '../live-event-bus.js';
|
|
11
11
|
import type { BudgetStatus } from '../../metrics/budget-tracker.js';
|
|
@@ -109,6 +109,13 @@ export interface ApiHandlerDeps {
|
|
|
109
109
|
getAgentCalls: (sessionId: string, agentId: string) => {
|
|
110
110
|
calls: readonly AgentCall[];
|
|
111
111
|
};
|
|
112
|
+
/**
|
|
113
|
+
* Live-run fallback for the workflow-detail route: a still-running workflow
|
|
114
|
+
* has no on-disk rollup, so `workflowStore.getRun` returns null; this
|
|
115
|
+
* assembles the detail from the run's live subagent transcripts instead.
|
|
116
|
+
* Optional so older fakes / mocks without it still type-check.
|
|
117
|
+
*/
|
|
118
|
+
getRunLive?: (runId: string) => LiveWorkflowRunDetail | null;
|
|
112
119
|
};
|
|
113
120
|
/**
|
|
114
121
|
* Snapshot of latest watcher health frames so the dashboard can render
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-handler.d.ts","sourceRoot":"","sources":["../../../src/dashboard/routes/api-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAK5D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iCAAiC,CAAC;AAC9E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAEL,KAAK,yBAAyB,EAC/B,MAAM,mCAAmC,CAAC;AAI3C,OAAO,KAAK,EAAuB,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAClF,OAAO,KAAK,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAC1F,OAAO,KAAK,EAAE,cAAc,EAAoB,MAAM,sBAAsB,CAAC;AAC7E,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"api-handler.d.ts","sourceRoot":"","sources":["../../../src/dashboard/routes/api-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -134,6 +134,55 @@ function toWorkflowAgentDto(agent) {
|
|
|
134
134
|
startedAt: a.started_at ?? null,
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
+
// Serialize a still-running workflow (assembled from live subagent transcripts,
|
|
138
|
+
// no on-disk rollup) into the SAME `{ run, agents, topology }` wire shape the
|
|
139
|
+
// completed path emits, so WorkflowRunDetail.tsx renders it unchanged. Fields
|
|
140
|
+
// that only exist post-rollup are filled with running-run defaults: status
|
|
141
|
+
// 'running', incomplete true, no error/task/reconciliation, and per-agent
|
|
142
|
+
// phase/state placeholders (phaseIndex -1, phaseTitle '', state 'running').
|
|
143
|
+
function toLiveWorkflowDetail(live) {
|
|
144
|
+
const run = {
|
|
145
|
+
runId: live.runId,
|
|
146
|
+
parentSessionId: live.parentSessionId,
|
|
147
|
+
taskId: null,
|
|
148
|
+
// Fall back to the runId when the script (and thus meta.name) can't be read.
|
|
149
|
+
workflowName: live.workflowName ?? live.runId,
|
|
150
|
+
status: 'running',
|
|
151
|
+
errorReason: null,
|
|
152
|
+
defaultModel: live.defaultModel,
|
|
153
|
+
startedAt: live.startedAt,
|
|
154
|
+
durationMs: live.durationMs,
|
|
155
|
+
agentCount: live.agentCount,
|
|
156
|
+
totalTokens: live.totalTokens,
|
|
157
|
+
totalUsd: live.totalUsd,
|
|
158
|
+
declaredPhases: live.topology?.declaredPhases ?? null,
|
|
159
|
+
// No phase-title telemetry exists mid-run (that comes from the rollup's
|
|
160
|
+
// workflowProgress), so observed phases are unknown → 0.
|
|
161
|
+
observedPhases: 0,
|
|
162
|
+
declaredParallelWidths: live.topology?.declaredParallelWidths ?? [],
|
|
163
|
+
tokenReconciliationDelta: 0,
|
|
164
|
+
incomplete: true,
|
|
165
|
+
// wf_<hex> runIds are script/Workflow-tool orchestrations (agent_tool runs
|
|
166
|
+
// key off toolu_* ids), so this is always a 'script' run.
|
|
167
|
+
runSource: 'script',
|
|
168
|
+
scriptPath: live.scriptPath,
|
|
169
|
+
workflowJsonPath: '',
|
|
170
|
+
};
|
|
171
|
+
const agents = live.agents.map((a) => ({
|
|
172
|
+
agentId: a.agentId,
|
|
173
|
+
label: a.label,
|
|
174
|
+
phaseIndex: -1,
|
|
175
|
+
phaseTitle: '',
|
|
176
|
+
model: a.model,
|
|
177
|
+
state: 'running',
|
|
178
|
+
attempt: 1,
|
|
179
|
+
durationMs: a.durationMs,
|
|
180
|
+
tokens: a.tokens,
|
|
181
|
+
toolCalls: a.toolCalls,
|
|
182
|
+
startedAt: a.startedAt,
|
|
183
|
+
}));
|
|
184
|
+
return { run, agents, topology: live.topology ?? null };
|
|
185
|
+
}
|
|
137
186
|
function jsonOk(res, body) {
|
|
138
187
|
const payload = JSON.stringify(body);
|
|
139
188
|
res.writeHead(200, {
|
|
@@ -189,78 +238,78 @@ function mergeActivityWindows(timeline) {
|
|
|
189
238
|
// computeTodayConcurrencyBuckets() below.
|
|
190
239
|
const CONCURRENCY_BUCKET_SIZE_MS = 15 * 60 * 1000;
|
|
191
240
|
const CONCURRENCY_BUCKET_COUNT = 96;
|
|
192
|
-
// Build
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
241
|
+
// Build activity windows for every session with activity today, using the SAME
|
|
242
|
+
// 3-minute activity-window model as computeTodayPeakConcurrency() (the source of
|
|
243
|
+
// the headline `peak`). Each session contributes one or more windows: contiguous
|
|
244
|
+
// tool-call activity merges into a single window, while an idle gap longer than
|
|
245
|
+
// ACTIVITY_WINDOW_MS splits a session into separate windows.
|
|
246
|
+
//
|
|
247
|
+
// This is what keeps the chart's tallest column equal to the headline peak. The
|
|
248
|
+
// earlier version used whole-session [startTime, endTime] spans (extending live
|
|
249
|
+
// sessions to `now`), which counted a long-idle-but-not-yet-ended session as
|
|
250
|
+
// continuously concurrent and inflated recent buckets well above the peak — the
|
|
251
|
+
// chart could read 7 while the headline read 4.
|
|
252
|
+
//
|
|
253
|
+
// Activity timestamps per session are unioned from BOTH the persisted timeline
|
|
254
|
+
// and the in-memory tool-call buffer (for live sessions whose latest activity
|
|
255
|
+
// hasn't been flushed to disk), then merged once per session id so a single
|
|
256
|
+
// session can never overlap itself.
|
|
257
|
+
function collectTodayActivityWindows(todaySessions, liveSessionIds, bufferRecords) {
|
|
258
|
+
const timestampsById = new Map();
|
|
259
|
+
const addTimestamps = (id, times) => {
|
|
260
|
+
const existing = timestampsById.get(id);
|
|
261
|
+
if (existing)
|
|
262
|
+
existing.push(...times);
|
|
263
|
+
else
|
|
264
|
+
timestampsById.set(id, [...times]);
|
|
265
|
+
};
|
|
201
266
|
for (const s of todaySessions) {
|
|
202
|
-
if (
|
|
203
|
-
continue;
|
|
204
|
-
const end = typeof s.endTime === 'number' ? s.endTime : now;
|
|
205
|
-
if (end <= s.startTime)
|
|
267
|
+
if (!s.timeline || s.timeline.length === 0)
|
|
206
268
|
continue;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
269
|
+
const times = s.timeline.map((t) => t.timestamp);
|
|
270
|
+
// `sessionId` is a required field on FullSessionSummary; the synthetic-key
|
|
271
|
+
// fallback only guards against malformed on-disk JSON (these records are
|
|
272
|
+
// read from disk, not schema-validated).
|
|
273
|
+
const id = typeof s.sessionId === 'string' && s.sessionId.length > 0
|
|
274
|
+
? s.sessionId
|
|
275
|
+
: `__anon_${timestampsById.size}`;
|
|
276
|
+
addTimestamps(id, times);
|
|
214
277
|
}
|
|
278
|
+
// Fold in live sessions' buffered (not-yet-persisted) activity, keyed by the
|
|
279
|
+
// same session id so it merges with the persisted timeline rather than
|
|
280
|
+
// double-counting a session that appears in both.
|
|
215
281
|
if (liveSessionIds.length > 0) {
|
|
216
|
-
const
|
|
282
|
+
const liveSet = new Set(liveSessionIds);
|
|
217
283
|
for (const r of bufferRecords) {
|
|
218
284
|
const sid = r.sessionId;
|
|
219
|
-
if (!sid)
|
|
285
|
+
if (!sid || !liveSet.has(sid))
|
|
220
286
|
continue;
|
|
221
287
|
const ts = r.timestamp ?? 0;
|
|
222
288
|
if (!ts)
|
|
223
289
|
continue;
|
|
224
|
-
|
|
225
|
-
if (prev === undefined || ts < prev)
|
|
226
|
-
firstTsBySession.set(sid, ts);
|
|
290
|
+
addTimestamps(sid, [ts]);
|
|
227
291
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// off the buffer's earliest record (which may be much later if the
|
|
234
|
-
// buffer was drained between flushes). Extend endTime to `now` —
|
|
235
|
-
// that's the original intent of this loop (a live session is, by
|
|
236
|
-
// definition, ongoing).
|
|
237
|
-
const candidates = [];
|
|
238
|
-
if (existing !== undefined)
|
|
239
|
-
candidates.push(existing.startTime);
|
|
240
|
-
if (bufferStart !== undefined)
|
|
241
|
-
candidates.push(bufferStart);
|
|
242
|
-
if (candidates.length === 0)
|
|
243
|
-
continue;
|
|
244
|
-
const startTime = Math.min(...candidates);
|
|
245
|
-
if (now <= startTime)
|
|
246
|
-
continue;
|
|
247
|
-
byId.set(sid, { startTime, endTime: now });
|
|
292
|
+
}
|
|
293
|
+
const windows = [];
|
|
294
|
+
for (const times of timestampsById.values()) {
|
|
295
|
+
for (const w of mergeActivityWindows(times.map((t) => ({ timestamp: t })))) {
|
|
296
|
+
windows.push({ startTime: w.start, endTime: w.end });
|
|
248
297
|
}
|
|
249
298
|
}
|
|
250
|
-
return
|
|
299
|
+
return windows;
|
|
251
300
|
}
|
|
252
|
-
function computeTodayConcurrencyBuckets(
|
|
253
|
-
// Single pass O(N log N) — build delta events from every
|
|
301
|
+
function computeTodayConcurrencyBuckets(windows, startTimestamp) {
|
|
302
|
+
// Single pass O(N log N) — build delta events from every activity window,
|
|
254
303
|
// clamped to today's window, then sort once and sweep across the 96
|
|
255
304
|
// buckets in lockstep with the event cursor. Buckets containing no
|
|
256
305
|
// events still need a count: the value carried over from the previous
|
|
257
|
-
// bucket's tail (a
|
|
306
|
+
// bucket's tail (a window that spans many buckets without a delta in
|
|
258
307
|
// between must propagate as count=1+ in those buckets).
|
|
259
308
|
const dayEnd = startTimestamp + CONCURRENCY_BUCKET_COUNT * CONCURRENCY_BUCKET_SIZE_MS;
|
|
260
309
|
const events = [];
|
|
261
|
-
for (const
|
|
262
|
-
const start = Math.max(
|
|
263
|
-
const end = Math.min(
|
|
310
|
+
for (const window of windows) {
|
|
311
|
+
const start = Math.max(window.startTime, startTimestamp);
|
|
312
|
+
const end = Math.min(window.endTime, dayEnd);
|
|
264
313
|
if (start < end) {
|
|
265
314
|
events.push({ ts: start, delta: 1 }, { ts: end, delta: -1 });
|
|
266
315
|
}
|
|
@@ -277,19 +326,29 @@ function computeTodayConcurrencyBuckets(intervals, startTimestamp) {
|
|
|
277
326
|
for (let b = 0; b < CONCURRENCY_BUCKET_COUNT; b++) {
|
|
278
327
|
const bucketStart = startTimestamp + b * CONCURRENCY_BUCKET_SIZE_MS;
|
|
279
328
|
const bucketEnd = bucketStart + CONCURRENCY_BUCKET_SIZE_MS;
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
329
|
+
// Peak defaults to the carried-over level so a session spanning many
|
|
330
|
+
// buckets with no events exactly at this boundary propagates as
|
|
331
|
+
// count=N+ here. But if one or more events DO land exactly on
|
|
332
|
+
// bucketStart, that default must be discarded rather than treated as a
|
|
333
|
+
// peak candidate — it reflects state from strictly BEFORE this bucket's
|
|
334
|
+
// range, not a value actually attained within it. (A session closing
|
|
335
|
+
// at exactly t=bucketStart is no longer active as of bucketStart; using
|
|
336
|
+
// the pre-close `current` as this bucket's floor would wrongly count it
|
|
337
|
+
// as still open here.) Once a boundary event fires, peak is instead
|
|
338
|
+
// sampled fresh after each one — same as the mid-bucket sweep below and
|
|
339
|
+
// as computeTodayPeakConcurrency's continuous sweep — so an exact-touch
|
|
340
|
+
// pair (one session's close coinciding with another's open, both at
|
|
341
|
+
// bucketStart) still registers the momentary overlap via the
|
|
342
|
+
// open-before-close tiebreaker, without resurrecting an already-closed
|
|
343
|
+
// session that has no such coincident open.
|
|
344
|
+
let peak = current;
|
|
345
|
+
let flushedAny = false;
|
|
286
346
|
while (cursor < events.length && events[cursor].ts <= bucketStart) {
|
|
287
347
|
current += events[cursor].delta;
|
|
348
|
+
peak = flushedAny ? Math.max(peak, current) : current;
|
|
349
|
+
flushedAny = true;
|
|
288
350
|
cursor++;
|
|
289
351
|
}
|
|
290
|
-
// Peak starts at the carried-over (post-flush) level. Sessions spanning
|
|
291
|
-
// many buckets with no mid-bucket events propagate as count=N+ here.
|
|
292
|
-
let peak = current;
|
|
293
352
|
while (cursor < events.length && events[cursor].ts < bucketEnd) {
|
|
294
353
|
current += events[cursor].delta;
|
|
295
354
|
if (current > peak)
|
|
@@ -1061,8 +1120,6 @@ export function createApiHandler(deps) {
|
|
|
1061
1120
|
if (!deps.concurrencyTracker)
|
|
1062
1121
|
return unavailable(res, 'concurrencyTracker');
|
|
1063
1122
|
try {
|
|
1064
|
-
const todaySessions = deps.sessionStore?.loadTodaySessions() ?? [];
|
|
1065
|
-
const historicalPeak = computeTodayPeakConcurrency(todaySessions);
|
|
1066
1123
|
const livePeak = deps.concurrencyTracker.getPeakConcurrent();
|
|
1067
1124
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
1068
1125
|
const view = url.searchParams.get('view');
|
|
@@ -1086,8 +1143,20 @@ export function createApiHandler(deps) {
|
|
|
1086
1143
|
jsonOk(res, { dailyPeaks });
|
|
1087
1144
|
return;
|
|
1088
1145
|
}
|
|
1146
|
+
// Synthetic session ids (`local-*`, `proxy-*`) are hidden from
|
|
1147
|
+
// /api/sessions and /api/sessions/live. Exclude them here too so
|
|
1148
|
+
// `allTimePeak` can't be inflated by sessions that never appear
|
|
1149
|
+
// anywhere else in the UI — it must stay comparable to `peak`, which
|
|
1150
|
+
// is filtered the same way below.
|
|
1089
1151
|
const allSessions = deps.sessionStore?.loadAllSessions?.() ?? [];
|
|
1090
|
-
const
|
|
1152
|
+
const filteredAllSessions = allSessions.filter((s) => !isSyntheticSessionId(s.sessionId));
|
|
1153
|
+
const allTimePeak = computeTodayPeakConcurrency(filteredAllSessions);
|
|
1154
|
+
// Synthetic session ids (`local-*`, `proxy-*`) are hidden from
|
|
1155
|
+
// /api/sessions and /api/sessions/live. Exclude them from the chart
|
|
1156
|
+
// buckets below so they agree with the session list shown alongside
|
|
1157
|
+
// the chart.
|
|
1158
|
+
const todaySessions = deps.sessionStore?.loadTodaySessions() ?? [];
|
|
1159
|
+
const filteredTodaySessions = todaySessions.filter((s) => !isSyntheticSessionId(s.sessionId));
|
|
1091
1160
|
// 96 × 15-minute fixed-grid buckets covering today (local midnight →
|
|
1092
1161
|
// local midnight + 24h). Each bucket holds the peak concurrent
|
|
1093
1162
|
// session count within its window. Bounded payload (~10 KB) and
|
|
@@ -1096,20 +1165,27 @@ export function createApiHandler(deps) {
|
|
|
1096
1165
|
const startTimestamp = localStartOfDay();
|
|
1097
1166
|
const liveIds = deps.liveSessionRegistry?.getLiveSessions() ?? [];
|
|
1098
1167
|
const bufferRecords = deps.toolCallBuffer?.getRecords() ?? [];
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1168
|
+
const windows = collectTodayActivityWindows(filteredTodaySessions, liveIds.filter((id) => !isSyntheticSessionId(id)), bufferRecords);
|
|
1169
|
+
const buckets = computeTodayConcurrencyBuckets(windows, startTimestamp);
|
|
1170
|
+
// Derive the headline "today peak" from the SAME buckets shown in the
|
|
1171
|
+
// chart — rather than from an independent computation over a
|
|
1172
|
+
// differently-scoped session set — so the two numbers can never
|
|
1173
|
+
// disagree by construction. (This was the root cause of the bug this
|
|
1174
|
+
// fix addresses: the headline and the chart used to be computed from
|
|
1175
|
+
// two different concurrency models that could silently drift apart.)
|
|
1176
|
+
//
|
|
1177
|
+
// Deliberately NOT folded with `livePeak` here (unlike allTimePeak
|
|
1178
|
+
// below): `livePeak` is a synthetic-id-unfiltered, never-reset
|
|
1179
|
+
// lifetime max from LiveSessionRegistry, so mixing it into today's
|
|
1180
|
+
// headline can reintroduce a chart/headline disagreement — the exact
|
|
1181
|
+
// class of bug this fix exists to eliminate. It's unnecessary anyway:
|
|
1182
|
+
// `windows` already unions each live session's buffered (not-yet-
|
|
1183
|
+
// persisted) activity into the same buckets, so a currently-live
|
|
1184
|
+
// session with any tool-call activity is already reflected here.
|
|
1185
|
+
const historicalPeak = buckets.reduce((max, b) => Math.max(max, b.count), 0);
|
|
1110
1186
|
jsonOk(res, {
|
|
1111
1187
|
current: deps.concurrencyTracker.getConcurrentCount(),
|
|
1112
|
-
peak:
|
|
1188
|
+
peak: historicalPeak,
|
|
1113
1189
|
allTimePeak: Math.max(livePeak, historicalPeak, allTimePeak),
|
|
1114
1190
|
bucketSizeMs: CONCURRENCY_BUCKET_SIZE_MS,
|
|
1115
1191
|
startTimestamp,
|
|
@@ -1490,6 +1566,17 @@ export function createApiHandler(deps) {
|
|
|
1490
1566
|
return unavailable(res, 'workflowStore');
|
|
1491
1567
|
const run = deps.workflowStore.getRun(runId);
|
|
1492
1568
|
if (run === null) {
|
|
1569
|
+
// Live fallback: a still-running workflow has no on-disk `wf_*.json`
|
|
1570
|
+
// rollup yet (that is written only at termination), but its subagent
|
|
1571
|
+
// transcripts already exist and the swimlane surfaces the run as
|
|
1572
|
+
// clickable. Synthesize a 'running' detail from those transcripts so
|
|
1573
|
+
// the drawer shows live progress instead of "Failed to load run
|
|
1574
|
+
// details." Falls through to 404 only when no live data exists either.
|
|
1575
|
+
const live = deps.subagentTimeline?.getRunLive?.(runId) ?? null;
|
|
1576
|
+
if (live !== null) {
|
|
1577
|
+
jsonOk(res, toLiveWorkflowDetail(live));
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1493
1580
|
res.writeHead(404, { 'content-type': 'application/json' });
|
|
1494
1581
|
res.end(JSON.stringify({ error: 'not_found' }));
|
|
1495
1582
|
return;
|