@newrelic/preflight 1.4.47 → 1.5.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.
Files changed (75) hide show
  1. package/dist/alerts/local-alert-engine.d.ts +1 -1
  2. package/dist/alerts/local-alert-engine.d.ts.map +1 -1
  3. package/dist/alerts/local-alert-engine.js +2 -2
  4. package/dist/alerts/local-alert-engine.js.map +1 -1
  5. package/dist/dashboard/dashboard-server.d.ts +2 -2
  6. package/dist/dashboard/dashboard-server.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-server.js +2 -2
  8. package/dist/dashboard/dashboard-server.js.map +1 -1
  9. package/dist/dashboard/index.d.ts +1 -1
  10. package/dist/dashboard/index.d.ts.map +1 -1
  11. package/dist/dashboard/live-event-bus.d.ts +32 -0
  12. package/dist/dashboard/live-event-bus.d.ts.map +1 -1
  13. package/dist/dashboard/live-event-bus.js.map +1 -1
  14. package/dist/dashboard/routes/api-handler.d.ts +46 -0
  15. package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
  16. package/dist/dashboard/routes/api-handler.js +188 -5
  17. package/dist/dashboard/routes/api-handler.js.map +1 -1
  18. package/dist/dashboard/subagent-timeline-store.d.ts +156 -0
  19. package/dist/dashboard/subagent-timeline-store.d.ts.map +1 -0
  20. package/dist/dashboard/subagent-timeline-store.js +674 -0
  21. package/dist/dashboard/subagent-timeline-store.js.map +1 -0
  22. package/dist/dashboard/workflow-store.d.ts +85 -0
  23. package/dist/dashboard/workflow-store.d.ts.map +1 -0
  24. package/dist/dashboard/workflow-store.js +330 -0
  25. package/dist/dashboard/workflow-store.js.map +1 -0
  26. package/dist/hooks/event-processor.d.ts +86 -1
  27. package/dist/hooks/event-processor.d.ts.map +1 -1
  28. package/dist/hooks/event-processor.js +182 -0
  29. package/dist/hooks/event-processor.js.map +1 -1
  30. package/dist/hooks/subagent-watcher.d.ts +187 -0
  31. package/dist/hooks/subagent-watcher.d.ts.map +1 -0
  32. package/dist/hooks/subagent-watcher.js +797 -0
  33. package/dist/hooks/subagent-watcher.js.map +1 -0
  34. package/dist/hooks/workflow-script-parser.d.ts +44 -0
  35. package/dist/hooks/workflow-script-parser.d.ts.map +1 -0
  36. package/dist/hooks/workflow-script-parser.js +255 -0
  37. package/dist/hooks/workflow-script-parser.js.map +1 -0
  38. package/dist/hooks/workflow-watcher.d.ts +67 -0
  39. package/dist/hooks/workflow-watcher.d.ts.map +1 -0
  40. package/dist/hooks/workflow-watcher.js +453 -0
  41. package/dist/hooks/workflow-watcher.js.map +1 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +316 -12
  44. package/dist/index.js.map +1 -1
  45. package/dist/metrics/cost-tracker.d.ts +113 -17
  46. package/dist/metrics/cost-tracker.d.ts.map +1 -1
  47. package/dist/metrics/cost-tracker.js +151 -8
  48. package/dist/metrics/cost-tracker.js.map +1 -1
  49. package/dist/metrics/recommendation-engine.d.ts +1 -1
  50. package/dist/metrics/recommendation-engine.js +1 -1
  51. package/dist/metrics/workflow-run-tracker.d.ts +140 -0
  52. package/dist/metrics/workflow-run-tracker.d.ts.map +1 -0
  53. package/dist/metrics/workflow-run-tracker.js +306 -0
  54. package/dist/metrics/workflow-run-tracker.js.map +1 -0
  55. package/dist/storage/local-store.d.ts +1 -1
  56. package/dist/storage/local-store.js +2 -2
  57. package/dist/storage/local-store.js.map +1 -1
  58. package/dist/storage/session-store.d.ts +8 -0
  59. package/dist/storage/session-store.d.ts.map +1 -1
  60. package/dist/storage/session-store.js +1 -1
  61. package/dist/storage/session-store.js.map +1 -1
  62. package/dist/storage/types.d.ts +40 -1
  63. package/dist/storage/types.d.ts.map +1 -1
  64. package/dist/tools/extended-analytics-tools.d.ts +1 -1
  65. package/dist/tools/extended-analytics-tools.js +1 -1
  66. package/dist/transport/nr-ingest.d.ts +153 -2
  67. package/dist/transport/nr-ingest.d.ts.map +1 -1
  68. package/dist/transport/nr-ingest.js +275 -0
  69. package/dist/transport/nr-ingest.js.map +1 -1
  70. package/dist/web/assets/index-B4DkT4Go.css +2 -0
  71. package/dist/web/assets/index-g9I2w7LV.js +64 -0
  72. package/dist/web/index.html +2 -2
  73. package/package.json +1 -1
  74. package/dist/web/assets/index-CrBs4WEp.js +0 -64
  75. package/dist/web/assets/index-DwBQxRYb.css +0 -2
@@ -0,0 +1,674 @@
1
+ /**
2
+ * SubagentTimelineStore — on-demand, bounded, mtime-cached reader that turns one
3
+ * session's Claude Code subagent JSONL transcripts into per-agent timing/token/
4
+ * cost spans for the dashboard's "agent fan-out" swimlane chart
5
+ * (`GET /api/sessions/:sessionId/subagents`).
6
+ *
7
+ * Discovery mirrors `SubagentWatcher`:
8
+ * - ad-hoc: `<projectsDir>/<slug>/<sessionId>/subagents/agent-*.jsonl`
9
+ * - workflow-spawned: `<projectsDir>/<slug>/<sessionId>/subagents/workflows/wf_<id>/agent-*.jsonl`
10
+ *
11
+ * Each `agent-<id>.jsonl` file is parsed line-by-line; the assistant turns
12
+ * (`type:'assistant'` with `message.model` + `message.usage`) supply the start/
13
+ * end timestamps, the model, the turn count, and the summed token usage. Per-
14
+ * agent USD is computed via the shared `calculateCost` pricing table; when the
15
+ * model is unknown (pricing resolves to an all-zero breakdown) `usd` is `null`
16
+ * — mirroring how `CostTracker` treats unknown models.
17
+ *
18
+ * Bounds (the watcher previously OOM'd on unbounded reads):
19
+ * - skip agent files larger than `MAX_AGENT_FILE_BYTES` (64 MiB),
20
+ * - cap the number of agents per session at `MAX_AGENTS_PER_SESSION` (500),
21
+ * - parse one line at a time without retaining the whole parsed set,
22
+ * - mtime-cache the computed `AgentSpan` per file so repeated dashboard polls
23
+ * don't re-parse unchanged transcripts.
24
+ *
25
+ * Privacy: this store NEVER reads `agent-<id>.meta.json` (which may
26
+ * contain the agent prompt) and NEVER includes prompt / content / result text.
27
+ * The only labels it emits are declarative — derived from the workflow rollup
28
+ * (`wf_*.json` workflowProgress) or a synthetic `agent <id8>` fallback.
29
+ *
30
+ * Every filesystem access and `JSON.parse` is wrapped in try/catch; the store
31
+ * never throws out of its public method.
32
+ */
33
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
34
+ import { homedir } from 'node:os';
35
+ import { join } from 'node:path';
36
+ import { calculateCost, createLogger } from '../shared/index.js';
37
+ import { WorkflowStore } from './workflow-store.js';
38
+ const logger = createLogger('subagent-timeline-store');
39
+ const SESSION_ID_RE = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
40
+ const AGENT_ID_RE = /^a[a-f0-9]{16}$/;
41
+ const WORKFLOW_RUN_ID_RE = /^wf_[A-Za-z0-9_-]{1,128}$/;
42
+ const PROJECTS_DIR_NAME = '.claude/projects';
43
+ /** Skip agent files larger than this — a transcript this big is pathological. */
44
+ const MAX_AGENT_FILE_BYTES = 64 * 1024 * 1024; // 64 MiB
45
+ /** Hard cap on distinct file paths retained per cache — bounds memory on a long-running dashboard server. */
46
+ const MAX_CACHE_ENTRIES = 4096;
47
+ /** Hard cap on agents returned per session. */
48
+ const MAX_AGENTS_PER_SESSION = 500;
49
+ /** Hard cap on individual tool calls returned for a single agent. */
50
+ const MAX_CALLS_PER_AGENT = 2000;
51
+ export class SubagentTimelineStore {
52
+ projectsDir;
53
+ injectedWorkflowStore;
54
+ workflowStore = null;
55
+ /** mtime-gated per-file parse cache, keyed by absolute file path. */
56
+ cache = new Map();
57
+ /** mtime-gated per-file tool-call cache, keyed by absolute file path. */
58
+ callsCache = new Map();
59
+ constructor(options = {}) {
60
+ this.projectsDir = options.projectsDir ?? join(homedir(), PROJECTS_DIR_NAME);
61
+ this.injectedWorkflowStore = options.workflowStore;
62
+ }
63
+ /**
64
+ * Set on a Map-based cache, evicting the oldest entry (insertion order)
65
+ * once `maxSize` is exceeded — bounds memory on a long-running dashboard
66
+ * server with many distinct agent file paths seen over time.
67
+ */
68
+ static boundedSet(map, key, value, maxSize) {
69
+ map.set(key, value);
70
+ if (map.size > maxSize) {
71
+ const oldest = map.keys().next().value;
72
+ if (oldest !== undefined)
73
+ map.delete(oldest);
74
+ }
75
+ }
76
+ /**
77
+ * Return per-agent swimlane spans for a single session. Always returns a
78
+ * value — `{ window: { startMs: 0, endMs: 0 }, agents: [] }` when the
79
+ * session has no subagent transcripts (or anything goes wrong).
80
+ */
81
+ getSubagentsForSession(sessionId) {
82
+ const empty = { window: { startMs: 0, endMs: 0 }, agents: [] };
83
+ if (!SESSION_ID_RE.test(sessionId))
84
+ return empty;
85
+ let files;
86
+ try {
87
+ files = this.discoverAgentFiles(sessionId);
88
+ }
89
+ catch (err) {
90
+ this.recordError('discover', err);
91
+ return empty;
92
+ }
93
+ if (files.length === 0)
94
+ return empty;
95
+ // Per-run workflow-name + per-agent label cache so each wf_*.json is read
96
+ // at most once per request even when a run spawned many agents.
97
+ const workflowNameByRun = new Map();
98
+ const labelByRunAgent = new Map();
99
+ const agents = [];
100
+ for (const file of files) {
101
+ if (agents.length >= MAX_AGENTS_PER_SESSION)
102
+ break;
103
+ const parsed = this.parseAgentFile(file.path);
104
+ if (parsed === null)
105
+ continue;
106
+ let workflowName = null;
107
+ let label = null;
108
+ if (file.workflowRunId !== null) {
109
+ const resolved = this.resolveWorkflow(file.workflowRunId, file.agentId, workflowNameByRun, labelByRunAgent);
110
+ workflowName = resolved.workflowName;
111
+ label = resolved.label;
112
+ }
113
+ if (label === null || label.length === 0) {
114
+ label = `agent ${file.agentId.slice(0, 8)}`;
115
+ }
116
+ const usd = computeUsd(parsed.usage, parsed.model);
117
+ agents.push({
118
+ agentId: file.agentId,
119
+ workflowRunId: file.workflowRunId,
120
+ workflowName,
121
+ label,
122
+ model: parsed.model,
123
+ startMs: parsed.startMs,
124
+ endMs: parsed.endMs,
125
+ durationMs: Math.max(0, parsed.endMs - parsed.startMs),
126
+ turnCount: parsed.turnCount,
127
+ totalTokens: parsed.totalTokens,
128
+ usd,
129
+ });
130
+ }
131
+ if (agents.length === 0)
132
+ return empty;
133
+ agents.sort((a, b) => a.startMs - b.startMs);
134
+ let windowStart = Infinity;
135
+ let windowEnd = -Infinity;
136
+ for (const a of agents) {
137
+ if (a.startMs < windowStart)
138
+ windowStart = a.startMs;
139
+ if (a.endMs > windowEnd)
140
+ windowEnd = a.endMs;
141
+ }
142
+ return {
143
+ window: {
144
+ startMs: Number.isFinite(windowStart) ? windowStart : 0,
145
+ endMs: Number.isFinite(windowEnd) ? windowEnd : 0,
146
+ },
147
+ agents,
148
+ };
149
+ }
150
+ /**
151
+ * Return ONE subagent's individual tool calls, for the attributed
152
+ * session-trace view. Always returns a value — `{ calls: [] }` for a bad
153
+ * session/agent id, a missing transcript, an oversized file, or any error.
154
+ *
155
+ * Privacy: only `toolName` + timing/outcome is emitted; tool inputs,
156
+ * outputs, prompts and `agent-*.meta.json` are never read or included.
157
+ */
158
+ getAgentCalls(sessionId, agentId) {
159
+ const empty = { calls: [] };
160
+ // Strict validation up front — reject anything that isn't a real session
161
+ // UUID / agent id so a crafted id can never escape the projects tree.
162
+ if (!SESSION_ID_RE.test(sessionId))
163
+ return empty;
164
+ if (!AGENT_ID_RE.test(agentId))
165
+ return empty;
166
+ let files;
167
+ try {
168
+ files = this.discoverAgentFiles(sessionId);
169
+ }
170
+ catch (err) {
171
+ this.recordError('discover calls', err);
172
+ return empty;
173
+ }
174
+ const file = files.find((f) => f.agentId === agentId);
175
+ if (file === undefined)
176
+ return empty;
177
+ const calls = this.parseAgentCalls(file.path);
178
+ return { calls };
179
+ }
180
+ // -------------------------------------------------------------------------
181
+ // Discovery
182
+ // -------------------------------------------------------------------------
183
+ discoverAgentFiles(sessionId) {
184
+ const out = [];
185
+ if (!existsSync(this.projectsDir))
186
+ return out;
187
+ let projectEntries;
188
+ try {
189
+ projectEntries = readdirSync(this.projectsDir);
190
+ }
191
+ catch (err) {
192
+ this.recordError('readdir projects', err);
193
+ return out;
194
+ }
195
+ for (const project of projectEntries) {
196
+ const subDir = join(this.projectsDir, project, sessionId, 'subagents');
197
+ if (!existsSync(subDir))
198
+ continue;
199
+ // Ad-hoc: subagents/agent-*.jsonl
200
+ this.collectAgentFiles(subDir, null, out);
201
+ // Workflow-spawned: subagents/workflows/wf_*/agent-*.jsonl
202
+ const wfDir = join(subDir, 'workflows');
203
+ if (!existsSync(wfDir))
204
+ continue;
205
+ let wfEntries;
206
+ try {
207
+ wfEntries = readdirSync(wfDir);
208
+ }
209
+ catch {
210
+ continue;
211
+ }
212
+ for (const wfName of wfEntries) {
213
+ if (!WORKFLOW_RUN_ID_RE.test(wfName))
214
+ continue;
215
+ const wfRunDir = join(wfDir, wfName);
216
+ try {
217
+ if (!statSync(wfRunDir).isDirectory())
218
+ continue;
219
+ }
220
+ catch {
221
+ continue;
222
+ }
223
+ this.collectAgentFiles(wfRunDir, wfName, out);
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+ /** Push every well-named `agent-<id>.jsonl` in `dir` onto `out`. */
229
+ collectAgentFiles(dir, workflowRunId, out) {
230
+ let names;
231
+ try {
232
+ names = readdirSync(dir);
233
+ }
234
+ catch {
235
+ return;
236
+ }
237
+ for (const name of names) {
238
+ if (!name.startsWith('agent-') || !name.endsWith('.jsonl'))
239
+ continue;
240
+ const agentId = name.slice('agent-'.length, -'.jsonl'.length);
241
+ if (!AGENT_ID_RE.test(agentId))
242
+ continue;
243
+ out.push({ path: join(dir, name), agentId, workflowRunId });
244
+ }
245
+ }
246
+ // -------------------------------------------------------------------------
247
+ // Parsing (mtime-cached, bounded)
248
+ // -------------------------------------------------------------------------
249
+ parseAgentFile(path) {
250
+ let st;
251
+ try {
252
+ st = statSync(path);
253
+ }
254
+ catch {
255
+ return null;
256
+ }
257
+ const cached = this.cache.get(path);
258
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
259
+ return cached.parsed;
260
+ }
261
+ if (st.size > MAX_AGENT_FILE_BYTES) {
262
+ logger.warn('Subagent transcript exceeds size cap — skipped', {
263
+ path,
264
+ sizeBytes: st.size,
265
+ maxBytes: MAX_AGENT_FILE_BYTES,
266
+ });
267
+ // Cache the skip so we don't re-stat-and-reject on every poll until the
268
+ // file changes again.
269
+ SubagentTimelineStore.boundedSet(this.cache, path, { mtimeMs: st.mtimeMs, size: st.size, parsed: null }, MAX_CACHE_ENTRIES);
270
+ return null;
271
+ }
272
+ let raw;
273
+ try {
274
+ raw = readFileSync(path, 'utf-8');
275
+ }
276
+ catch (err) {
277
+ this.recordError('read agent file', err);
278
+ return null;
279
+ }
280
+ const parsed = this.parseTranscript(raw);
281
+ SubagentTimelineStore.boundedSet(this.cache, path, { mtimeMs: st.mtimeMs, size: st.size, parsed }, MAX_CACHE_ENTRIES);
282
+ return parsed;
283
+ }
284
+ /**
285
+ * Parse a JSONL transcript line-by-line, accumulating only scalar totals —
286
+ * never retaining the parsed objects. Returns null when no assistant turns
287
+ * with usage are found.
288
+ */
289
+ parseTranscript(raw) {
290
+ let startMs = Infinity;
291
+ let endMs = -Infinity;
292
+ let turnCount = 0;
293
+ let inputTokens = 0;
294
+ let outputTokens = 0;
295
+ let cacheReadTokens = 0;
296
+ let cacheCreationTokens = 0;
297
+ let lastModel = '';
298
+ // Dedup streaming-duplicate lines: Claude Code logs one JSONL line per
299
+ // streaming snapshot of a single assistant turn, all sharing one
300
+ // `message.id` with byte-identical per-prompt usage. Summing every line
301
+ // multiplies cache_read/cache_creation by the snapshot count (the dominant
302
+ // token category, ~90%+ of the total), inflating both `totalTokens` and the
303
+ // derived USD ~2x. We count each `message.id` once, keeping the FIRST
304
+ // occurrence — identical to the cost path's `${agentId}|${messageId}` dedup
305
+ // in event-processor.ts — so this trace reconciles with the headline cost.
306
+ const seenMessageIds = new Set();
307
+ let lineStart = 0;
308
+ const len = raw.length;
309
+ while (lineStart <= len) {
310
+ let nl = raw.indexOf('\n', lineStart);
311
+ if (nl === -1)
312
+ nl = len;
313
+ const line = raw.slice(lineStart, nl);
314
+ lineStart = nl + 1;
315
+ if (line.length === 0) {
316
+ if (nl === len)
317
+ break;
318
+ continue;
319
+ }
320
+ const turn = parseAssistantTurn(line);
321
+ if (turn === null) {
322
+ if (nl === len)
323
+ break;
324
+ continue;
325
+ }
326
+ if (seenMessageIds.has(turn.messageId)) {
327
+ if (nl === len)
328
+ break;
329
+ continue;
330
+ }
331
+ seenMessageIds.add(turn.messageId);
332
+ turnCount += 1;
333
+ if (turn.timestampMs < startMs)
334
+ startMs = turn.timestampMs;
335
+ if (turn.timestampMs > endMs)
336
+ endMs = turn.timestampMs;
337
+ inputTokens += turn.inputTokens;
338
+ outputTokens += turn.outputTokens;
339
+ cacheReadTokens += turn.cacheReadTokens;
340
+ cacheCreationTokens += turn.cacheCreationTokens;
341
+ if (turn.model.length > 0)
342
+ lastModel = turn.model;
343
+ if (nl === len)
344
+ break;
345
+ }
346
+ if (turnCount === 0 || !Number.isFinite(startMs) || !Number.isFinite(endMs)) {
347
+ return null;
348
+ }
349
+ const totalTokens = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
350
+ const usage = {
351
+ inputTokens,
352
+ outputTokens,
353
+ thinkingTokens: 0,
354
+ cacheReadTokens,
355
+ cacheCreationTokens,
356
+ totalTokens,
357
+ };
358
+ return { startMs, endMs, turnCount, totalTokens, usage, model: lastModel };
359
+ }
360
+ // -------------------------------------------------------------------------
361
+ // Per-agent tool-call parsing (mtime-cached, bounded, privacy-preserving)
362
+ // -------------------------------------------------------------------------
363
+ /**
364
+ * Parse one agent transcript into its tool-call list, mtime-cached and size-
365
+ * capped exactly like `parseAgentFile`. Always returns an array (possibly
366
+ * empty); never throws.
367
+ */
368
+ parseAgentCalls(path) {
369
+ let st;
370
+ try {
371
+ st = statSync(path);
372
+ }
373
+ catch {
374
+ return [];
375
+ }
376
+ const cached = this.callsCache.get(path);
377
+ if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
378
+ return cached.calls;
379
+ }
380
+ if (st.size > MAX_AGENT_FILE_BYTES) {
381
+ logger.warn('Subagent transcript exceeds size cap — calls skipped', {
382
+ path,
383
+ sizeBytes: st.size,
384
+ maxBytes: MAX_AGENT_FILE_BYTES,
385
+ });
386
+ SubagentTimelineStore.boundedSet(this.callsCache, path, { mtimeMs: st.mtimeMs, size: st.size, calls: [] }, MAX_CACHE_ENTRIES);
387
+ return [];
388
+ }
389
+ let raw;
390
+ try {
391
+ raw = readFileSync(path, 'utf-8');
392
+ }
393
+ catch (err) {
394
+ this.recordError('read agent calls', err);
395
+ return [];
396
+ }
397
+ const calls = parseToolCalls(raw);
398
+ SubagentTimelineStore.boundedSet(this.callsCache, path, { mtimeMs: st.mtimeMs, size: st.size, calls }, MAX_CACHE_ENTRIES);
399
+ return calls;
400
+ }
401
+ // -------------------------------------------------------------------------
402
+ // Workflow label resolution (declarative data only)
403
+ // -------------------------------------------------------------------------
404
+ resolveWorkflow(workflowRunId, agentId, workflowNameByRun, labelByRunAgent) {
405
+ const labelKey = `${workflowRunId} ${agentId}`;
406
+ if (workflowNameByRun.has(workflowRunId)) {
407
+ return {
408
+ workflowName: workflowNameByRun.get(workflowRunId) ?? null,
409
+ label: labelByRunAgent.get(labelKey) ?? null,
410
+ };
411
+ }
412
+ let workflowName = null;
413
+ try {
414
+ const store = this.getWorkflowStore();
415
+ const run = store.getRun(workflowRunId);
416
+ if (run) {
417
+ workflowName =
418
+ typeof run.workflow_name === 'string' && run.workflow_name.length > 0
419
+ ? run.workflow_name
420
+ : null;
421
+ if (Array.isArray(run.agents)) {
422
+ for (const a of run.agents) {
423
+ if (typeof a.agent_id !== 'string')
424
+ continue;
425
+ const lbl = typeof a.label === 'string' && a.label.length > 0 ? a.label : '';
426
+ if (lbl)
427
+ labelByRunAgent.set(`${workflowRunId} ${a.agent_id}`, lbl);
428
+ }
429
+ }
430
+ }
431
+ }
432
+ catch (err) {
433
+ this.recordError('resolve workflow', err);
434
+ }
435
+ workflowNameByRun.set(workflowRunId, workflowName);
436
+ return { workflowName, label: labelByRunAgent.get(labelKey) ?? null };
437
+ }
438
+ getWorkflowStore() {
439
+ if (this.injectedWorkflowStore)
440
+ return this.injectedWorkflowStore;
441
+ if (this.workflowStore === null) {
442
+ this.workflowStore = new WorkflowStore({ projectsDir: this.projectsDir });
443
+ }
444
+ return this.workflowStore;
445
+ }
446
+ recordError(stage, err) {
447
+ logger.warn('SubagentTimelineStore error', {
448
+ stage,
449
+ error: err instanceof Error ? err.message.slice(0, 200) : String(err).slice(0, 200),
450
+ });
451
+ }
452
+ }
453
+ /**
454
+ * Parse one JSONL line into an assistant turn, or null when the line is
455
+ * malformed or not an assistant turn with usage. Never throws.
456
+ */
457
+ function parseAssistantTurn(line) {
458
+ let parsed;
459
+ try {
460
+ parsed = JSON.parse(line);
461
+ }
462
+ catch {
463
+ return null;
464
+ }
465
+ if (!parsed || typeof parsed !== 'object')
466
+ return null;
467
+ const obj = parsed;
468
+ if (obj.type !== 'assistant')
469
+ return null;
470
+ const message = obj.message;
471
+ if (!message || typeof message !== 'object')
472
+ return null;
473
+ const m = message;
474
+ const model = typeof m.model === 'string' ? m.model : '';
475
+ // `<synthetic>` turns carry no real usage and no priceable model.
476
+ if (model === '<synthetic>')
477
+ return null;
478
+ // Claude Code logs one JSONL line per streaming snapshot of a single
479
+ // assistant turn, all sharing one `message.id` with byte-identical per-prompt
480
+ // usage (cache_read/cache_creation/input). We require the id so duplicate
481
+ // snapshots can be deduped in parseTranscript — mirroring the cost path's
482
+ // `${agentId}|${messageId}` dedup in event-processor.ts. Lines without an id
483
+ // are skipped, exactly as the CostTracker feed (SubagentWatcher) skips them.
484
+ const messageId = typeof m.id === 'string' ? m.id : '';
485
+ if (messageId.length === 0)
486
+ return null;
487
+ const usage = m.usage;
488
+ if (!usage || typeof usage !== 'object')
489
+ return null;
490
+ const u = usage;
491
+ const tsRaw = typeof obj.timestamp === 'string' ? obj.timestamp : null;
492
+ const timestampMs = tsRaw ? Date.parse(tsRaw) : NaN;
493
+ if (!Number.isFinite(timestampMs))
494
+ return null;
495
+ return {
496
+ messageId,
497
+ timestampMs,
498
+ model,
499
+ inputTokens: num(u.input_tokens),
500
+ outputTokens: num(u.output_tokens),
501
+ cacheReadTokens: num(u.cache_read_input_tokens),
502
+ cacheCreationTokens: num(u.cache_creation_input_tokens),
503
+ };
504
+ }
505
+ function num(v) {
506
+ return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : 0;
507
+ }
508
+ /**
509
+ * Compute per-agent USD via the shared pricing table. `calculateCost` returns
510
+ * an all-zero breakdown for unknown models (it never returns null), so we treat
511
+ * a `totalUsd` of 0 with no real spend as "unknown / unpriced" → null, matching
512
+ * how CostTracker reports unknown models. A genuinely zero-token agent (no
513
+ * billable usage) also yields null, which is the honest answer.
514
+ */
515
+ function computeUsd(usage, model) {
516
+ if (model.length === 0)
517
+ return null;
518
+ const breakdown = calculateCost(model, usage);
519
+ if (breakdown.totalUsd > 0)
520
+ return breakdown.totalUsd;
521
+ return null;
522
+ }
523
+ /**
524
+ * Walk a transcript line-by-line and build the ordered list of tool calls.
525
+ *
526
+ * For each assistant turn, every `tool_use` content block is opened with its
527
+ * issuing turn's timestamp; the matching `tool_result` block (in a LATER user
528
+ * turn, keyed by `tool_use_id`) supplies `success` (`!is_error`) and
529
+ * `durationMs` (result-turn timestamp − use-turn timestamp). A tool_use with no
530
+ * matching result keeps `durationMs: null` and `success: true`.
531
+ *
532
+ * Privacy: only `block.name` is read from a tool_use, and only
533
+ * `tool_use_id` + `is_error` from a tool_result. Tool `input`, result content,
534
+ * text blocks and prompts are never touched. Never throws.
535
+ */
536
+ function parseToolCalls(raw) {
537
+ const calls = [];
538
+ // Map tool_use_id → index into `calls`, so a later tool_result can patch the
539
+ // existing entry in place (preserving the issue-time ordering).
540
+ const indexByUseId = new Map();
541
+ let capped = false;
542
+ let lineStart = 0;
543
+ const len = raw.length;
544
+ while (lineStart <= len) {
545
+ let nl = raw.indexOf('\n', lineStart);
546
+ if (nl === -1)
547
+ nl = len;
548
+ const line = raw.slice(lineStart, nl);
549
+ lineStart = nl + 1;
550
+ if (line.length === 0) {
551
+ if (nl === len)
552
+ break;
553
+ continue;
554
+ }
555
+ const turn = parseTurnBlocks(line);
556
+ if (turn !== null) {
557
+ if (turn.role === 'assistant') {
558
+ for (const block of turn.blocks) {
559
+ const use = readToolUse(block);
560
+ if (use === null)
561
+ continue;
562
+ // Skip a tool_use already recorded under this id — streaming-duplicate
563
+ // snapshots of one assistant turn repeat the same tool_use block, and
564
+ // counting each repeat inflates the per-agent call list.
565
+ if (use.id.length > 0 && indexByUseId.has(use.id))
566
+ continue;
567
+ if (calls.length >= MAX_CALLS_PER_AGENT) {
568
+ capped = true;
569
+ break;
570
+ }
571
+ const pending = { toolName: use.name, timestamp: turn.timestamp };
572
+ const idx = calls.length;
573
+ calls.push({
574
+ toolName: pending.toolName,
575
+ timestamp: pending.timestamp,
576
+ durationMs: null,
577
+ success: true,
578
+ });
579
+ if (use.id.length > 0)
580
+ indexByUseId.set(use.id, idx);
581
+ }
582
+ }
583
+ else {
584
+ // user turn — look for tool_result blocks pairing back to a tool_use.
585
+ for (const block of turn.blocks) {
586
+ const result = readToolResult(block);
587
+ if (result === null)
588
+ continue;
589
+ const idx = indexByUseId.get(result.toolUseId);
590
+ if (idx === undefined)
591
+ continue;
592
+ const existing = calls[idx];
593
+ if (existing === undefined)
594
+ continue;
595
+ const durationMs = turn.timestamp >= existing.timestamp ? turn.timestamp - existing.timestamp : null;
596
+ calls[idx] = {
597
+ toolName: existing.toolName,
598
+ timestamp: existing.timestamp,
599
+ durationMs,
600
+ success: !result.isError,
601
+ };
602
+ }
603
+ }
604
+ }
605
+ if (capped || nl === len)
606
+ break;
607
+ }
608
+ calls.sort((a, b) => a.timestamp - b.timestamp);
609
+ return calls;
610
+ }
611
+ /**
612
+ * Parse one JSONL line into a role + timestamp + content-block array, or null
613
+ * when the line is malformed, not an assistant/user turn, has no parseable
614
+ * timestamp, or carries no array content. Never throws.
615
+ */
616
+ function parseTurnBlocks(line) {
617
+ let parsed;
618
+ try {
619
+ parsed = JSON.parse(line);
620
+ }
621
+ catch {
622
+ return null;
623
+ }
624
+ if (!parsed || typeof parsed !== 'object')
625
+ return null;
626
+ const obj = parsed;
627
+ const role = obj.type;
628
+ if (role !== 'assistant' && role !== 'user')
629
+ return null;
630
+ const tsRaw = typeof obj.timestamp === 'string' ? obj.timestamp : null;
631
+ const timestamp = tsRaw ? Date.parse(tsRaw) : NaN;
632
+ if (!Number.isFinite(timestamp))
633
+ return null;
634
+ const message = obj.message;
635
+ if (!message || typeof message !== 'object')
636
+ return null;
637
+ const content = message.content;
638
+ if (!Array.isArray(content))
639
+ return null;
640
+ return { role, timestamp, blocks: content };
641
+ }
642
+ /**
643
+ * Read a `tool_use` block, returning only its name + id (never the
644
+ * `input`). Returns null for any non-tool_use block or missing name.
645
+ */
646
+ function readToolUse(block) {
647
+ if (!block || typeof block !== 'object')
648
+ return null;
649
+ const b = block;
650
+ if (b.type !== 'tool_use')
651
+ return null;
652
+ const name = typeof b.name === 'string' ? b.name : '';
653
+ if (name.length === 0)
654
+ return null;
655
+ const id = typeof b.id === 'string' ? b.id : '';
656
+ return { name, id };
657
+ }
658
+ /**
659
+ * Read a `tool_result` block, returning only its `tool_use_id` + error flag
660
+ * (never the result content). Returns null for any non-tool_result
661
+ * block or missing id.
662
+ */
663
+ function readToolResult(block) {
664
+ if (!block || typeof block !== 'object')
665
+ return null;
666
+ const b = block;
667
+ if (b.type !== 'tool_result')
668
+ return null;
669
+ const toolUseId = typeof b.tool_use_id === 'string' ? b.tool_use_id : '';
670
+ if (toolUseId.length === 0)
671
+ return null;
672
+ return { toolUseId, isError: b.is_error === true };
673
+ }
674
+ //# sourceMappingURL=subagent-timeline-store.js.map