@newrelic/preflight 1.50.8 → 1.50.10

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 (42) hide show
  1. package/dist/alerts/local-alert-rule.d.ts +2 -2
  2. package/dist/dashboard/routes/api-handler.d.ts +5 -9
  3. package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
  4. package/dist/dashboard/routes/api-handler.js +12 -18
  5. package/dist/dashboard/routes/api-handler.js.map +1 -1
  6. package/dist/hooks/subagent-watcher.d.ts +42 -16
  7. package/dist/hooks/subagent-watcher.d.ts.map +1 -1
  8. package/dist/hooks/subagent-watcher.js +76 -13
  9. package/dist/hooks/subagent-watcher.js.map +1 -1
  10. package/dist/index.js +113 -76
  11. package/dist/index.js.map +1 -1
  12. package/dist/metrics/git-activity-recorder.d.ts +6 -5
  13. package/dist/metrics/git-activity-recorder.d.ts.map +1 -1
  14. package/dist/metrics/git-activity-recorder.js +39 -70
  15. package/dist/metrics/git-activity-recorder.js.map +1 -1
  16. package/dist/metrics/git-efficiency-tracker.d.ts +0 -1
  17. package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -1
  18. package/dist/metrics/git-efficiency-tracker.js +23 -57
  19. package/dist/metrics/git-efficiency-tracker.js.map +1 -1
  20. package/dist/metrics/git-event-classifier.d.ts +28 -1
  21. package/dist/metrics/git-event-classifier.d.ts.map +1 -1
  22. package/dist/metrics/git-event-classifier.js +57 -2
  23. package/dist/metrics/git-event-classifier.js.map +1 -1
  24. package/dist/metrics/git-workspace-report.d.ts +24 -1
  25. package/dist/metrics/git-workspace-report.d.ts.map +1 -1
  26. package/dist/metrics/git-workspace-report.js +141 -10
  27. package/dist/metrics/git-workspace-report.js.map +1 -1
  28. package/dist/metrics/git-workspace-reporter.d.ts +12 -0
  29. package/dist/metrics/git-workspace-reporter.d.ts.map +1 -1
  30. package/dist/metrics/git-workspace-reporter.js +43 -3
  31. package/dist/metrics/git-workspace-reporter.js.map +1 -1
  32. package/dist/metrics/local-session-aggregator.d.ts +59 -4
  33. package/dist/metrics/local-session-aggregator.d.ts.map +1 -1
  34. package/dist/metrics/local-session-aggregator.js +121 -13
  35. package/dist/metrics/local-session-aggregator.js.map +1 -1
  36. package/dist/storage/session-store.d.ts +32 -0
  37. package/dist/storage/session-store.d.ts.map +1 -1
  38. package/dist/storage/session-store.js +37 -2
  39. package/dist/storage/session-store.js.map +1 -1
  40. package/dist/web/assets/{index-CV85Em1K.js → index-BrrBgs1G.js} +1 -1
  41. package/dist/web/index.html +1 -1
  42. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -1082,6 +1082,10 @@ async function main() {
1082
1082
  getRecords: () => toolCallBuffer,
1083
1083
  };
1084
1084
  sessionStore = new SessionStore({ storagePath: config.storagePath });
1085
+ // Non-null capture so the aggregator's callback (invoked lazily, long
1086
+ // after this point) doesn't have to re-narrow `sessionStore: SessionStore
1087
+ // | undefined` — same pattern as sessionStoreForCostBaseline below.
1088
+ const sessionStoreForAggregator = sessionStore;
1085
1089
  const currentSessionId = sessionTracker.getMetrics().sessionId;
1086
1090
  let currentRepoName = null;
1087
1091
  // An unscoped process (--local, or a provisional --stdio window) drains
@@ -1089,7 +1093,13 @@ async function main() {
1089
1093
  // synthetic id, which persistSession() skips — so without this rollup the
1090
1094
  // sessions it observes are never written to disk at all. See
1091
1095
  // local-session-aggregator.ts for why that hits Copilot but not Claude Code.
1092
- const localSessionAggregator = new LocalSessionAggregator();
1096
+ const localSessionAggregator = new LocalSessionAggregator({
1097
+ // Restart survival: the first subagent turn this process sees for a
1098
+ // session folds in whatever that session's file already says (a
1099
+ // --stdio engine's final write, or this daemon's own last checkpoint)
1100
+ // before adding the tail — see applyPersistedBaseline's doc comment.
1101
+ persistedCostBaseline: (id) => sessionStoreForAggregator.loadSession(id),
1102
+ });
1093
1103
  const repoNameResolver = new RepoNameResolver();
1094
1104
  const budgetTracker = new BudgetTracker({
1095
1105
  sessionBudgetUsd: config.sessionBudgetUsd,
@@ -1264,10 +1274,18 @@ async function main() {
1264
1274
  const hydrateGitCommits = () => {
1265
1275
  // Recomputed per call so a long-lived dashboard rolls over at midnight
1266
1276
  // instead of reporting "today" relative to the day it was started.
1267
- const since = new Date().toISOString().slice(0, 10);
1268
- const commits = collectCommitsAcrossRepos(collectRepoRoots(), since, gitAuthorEmail);
1269
- if (commits.length > 0)
1270
- gitEfficiencyTracker.hydrateGitLog(commits);
1277
+ const todayStartMs = new Date().setHours(0, 0, 0, 0);
1278
+ // The Git tab's tree shows 30 days of history — the shared git-log
1279
+ // collection needs to reach that far even though the per-session
1280
+ // GitEfficiencyTracker below only ever wants today's commits.
1281
+ const hydrationSince = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
1282
+ .toISOString()
1283
+ .slice(0, 10);
1284
+ const commits = collectCommitsAcrossRepos(collectRepoRoots(), hydrationSince, gitAuthorEmail);
1285
+ gitWorkspaceReporter.hydrateGitLog(commits);
1286
+ const todaysCommits = commits.filter((c) => c.timestamp >= todayStartMs);
1287
+ if (todaysCommits.length > 0)
1288
+ gitEfficiencyTracker.hydrateGitLog(todaysCommits);
1271
1289
  };
1272
1290
  hydrateGitCommits();
1273
1291
  const gitHydrationInterval = setInterval(hydrateGitCommits, 5 * 60_000);
@@ -1571,22 +1589,12 @@ async function main() {
1571
1589
  observabilityHealth: {
1572
1590
  getSnapshot: () => {
1573
1591
  // Read live counters off the SubagentWatcher when it's running.
1574
- // A null binding => watcher disabled (env flag off / wrong mode)
1575
- // => a zeroed "disabled" snapshot. costSelfCheckDeltaPct stays
1576
- // null until the 1h self-check is wired.
1592
+ // A null binding => watcher disabled (env flag off) => a zeroed
1593
+ // "disabled" snapshot. costSelfCheckDeltaPct stays null until
1594
+ // the 1h self-check is wired.
1577
1595
  const stats = activeSubagentWatcher?.getHealthStats();
1578
1596
  const watcherActive = activeSubagentWatcher !== null;
1579
- // Re-derive which of the two independent conditions caused a
1580
- // null binding, rather than reading the `subagentWatcherEnabled`/
1581
- // `watcherShouldRun` consts from the outer closure — this way
1582
- // the reason is always self-consistent with the actual env var
1583
- // at snapshot time, with no scope-ordering dependency on where
1584
- // those consts are declared in this large startup function.
1585
- const watcherDisabledReason = watcherActive
1586
- ? null
1587
- : process.env['NR_AI_ENABLE_SUBAGENT_WATCHER'] === '0'
1588
- ? 'env_var'
1589
- : 'mode_mismatch';
1597
+ const watcherDisabledReason = watcherActive ? null : 'env_var';
1590
1598
  return {
1591
1599
  watcherActive,
1592
1600
  filesWatched: stats?.filesWatched ?? 0,
@@ -2112,6 +2120,22 @@ async function main() {
2112
2120
  // belong in the model breakdown too — recording them only in the cost
2113
2121
  // tracker left Model Usage blind to every subagent-only session.
2114
2122
  modelUsageTracker.recordUsage(turn.model, usage, breakdown.totalUsd);
2123
+ // The only line that makes an orphan session's subagent spend durable:
2124
+ // without it, an unscoped process's aggregator never learns about this
2125
+ // session's subagent cost and persistSession() has nothing to write.
2126
+ // `agentId` is the same discriminator costTracker.recordTokenUsage
2127
+ // splits parent-vs-subagent on above, so the two trackers can't drift.
2128
+ localSessionAggregator.recordTokenUsage(turn.parentSessionId, {
2129
+ timestamp: turn.timestampMs,
2130
+ costUsd: breakdown.totalUsd,
2131
+ model: turn.model,
2132
+ inputTokens: turn.inputTokens,
2133
+ outputTokens: turn.outputTokens,
2134
+ cacheReadTokens: turn.cacheReadTokens,
2135
+ cacheCreationTokens: turn.cacheCreationTokens,
2136
+ thinkingTokens: turn.reasoningTokens,
2137
+ agentId: turn.agentId,
2138
+ });
2115
2139
  // Pricing miss → usd:null on the wire; we recompute here so
2116
2140
  // the breakdown view distinguishes "0 because pricing absent" from
2117
2141
  // "0 because the turn truly had zero cost".
@@ -2326,13 +2350,7 @@ async function main() {
2326
2350
  }
2327
2351
  }, SESSION_PERSIST_INTERVAL_MS);
2328
2352
  sessionPersistInterval.unref?.();
2329
- // Single-mode rule: the watcher runs in `--stdio` mode by default.
2330
- // Opt-in to watcher-in-dashboard via `NR_AI_WATCHER_MODE=local`.
2331
- const watcherMode = (process.env['NR_AI_WATCHER_MODE'] ?? 'stdio').toLowerCase();
2332
2353
  const isStdioWatcher = options.stdio === true;
2333
- const isLocalWatcher = !isStdioWatcher;
2334
- const watcherShouldRun = (isStdioWatcher && (watcherMode === 'stdio' || watcherMode === '')) ||
2335
- (isLocalWatcher && watcherMode === 'local');
2336
2354
  // The SubagentWatcher is the ONLY thing that feeds per-agent (subagent)
2337
2355
  // token cost into the CostTracker (via onSubagentTurn → subagentCostUsd).
2338
2356
  // With it off, a session's persisted/headline cost silently excludes ALL
@@ -2343,13 +2361,18 @@ async function main() {
2343
2361
  // (parentSessionId filter), so it only ever attributes that session's own
2344
2362
  // subagents — parent tokens (onTokenEvent, parent transcript) and subagent
2345
2363
  // tokens (onSubagentTurn, subagent transcripts) are disjoint, so there is no
2346
- // double count. The WorkflowWatcher stays opt-in (NR_AI_ENABLE_WORKFLOW_WATCHER=1).
2364
+ // double count. Unfiltered (`--local`), the heartbeat exclusion
2365
+ // (subagent-watcher.ts's discoverFiles) is what keeps it from racing a live
2366
+ // `--stdio` session's own scoped watcher over the same cursor files — see
2367
+ // that module's doc comment. The WorkflowWatcher stays engine-only: it has
2368
+ // no heartbeat exclusion, so running it unfiltered would reintroduce that
2369
+ // exact race for workflow transcripts.
2347
2370
  const subagentWatcherEnabled = process.env['NR_AI_ENABLE_SUBAGENT_WATCHER'] !== '0';
2348
2371
  const workflowWatcherEnabled = process.env['NR_AI_ENABLE_WORKFLOW_WATCHER'] === '1';
2349
- // Unlike subagent/workflow watchers, ParentTranscriptWatcher is NOT gated
2350
- // by watcherShouldRun/NR_AI_WATCHER_MODE — see startWatchers() below for
2351
- // why. Do not "fix" this to match the other two; that would reintroduce
2352
- // the exact regression this divergence avoids.
2372
+ // Unlike WorkflowWatcher, ParentTranscriptWatcher is not gated
2373
+ // by --stdio-vs-`--local` at all — see startWatchers() below for why. Do
2374
+ // not "fix" this to match WorkflowWatcher; that would reintroduce the exact
2375
+ // regression this divergence avoids.
2353
2376
  const parentTranscriptWatcherEnabled = process.env['NR_AI_ENABLE_PARENT_TRANSCRIPT_WATCHER'] !== '0';
2354
2377
  // CopilotUsageWatcher is the Copilot analog of ParentTranscriptWatcher
2355
2378
  // (token-exact cost from VS Code's Copilot debug logs) and follows the
@@ -2375,15 +2398,14 @@ async function main() {
2375
2398
  // ParentTranscriptWatcher feeds parent-session token/cost tracking —
2376
2399
  // the primary cost signal, not a secondary one like subagent/workflow
2377
2400
  // cost. The old per-hook transcript scanner it replaces ran
2378
- // unconditionally in every mode with zero coupling to
2379
- // NR_AI_WATCHER_MODE; gating this behind watcherShouldRun the same way
2380
- // SubagentWatcher/WorkflowWatcher are gated would mean a standalone
2381
- // `--local` deployment (no --stdio sibling, NR_AI_WATCHER_MODE unset)
2382
- // goes from "buggy but nonzero" parent-cost tracking to "exactly zero"
2383
- // by default a real regression. So it always runs, gated only by its
2384
- // own opt-out flag. Race-safety for "--stdio and --local both alive for
2385
- // the same session" comes for free from the same
2386
- // getActiveSessionIdsFromHeartbeats() exclusion SubagentWatcher's
2401
+ // unconditionally in every mode with zero coupling to --stdio-vs-`--local`;
2402
+ // gating this behind isStdioWatcher the same way WorkflowWatcher is
2403
+ // gated would mean a standalone `--local` deployment (no --stdio
2404
+ // sibling) goes from "buggy but nonzero" parent-cost tracking to
2405
+ // "exactly zero" by default a real regression. So it always runs,
2406
+ // gated only by its own opt-out flag. Race-safety for "--stdio and
2407
+ // --local both alive for the same session" comes for free from the
2408
+ // same getActiveSessionIdsFromHeartbeats() exclusion SubagentWatcher's
2387
2409
  // unscoped discovery already uses.
2388
2410
  if (parentTranscriptWatcherEnabled) {
2389
2411
  activeParentTranscriptWatcher = new ParentTranscriptWatcher({
@@ -2418,50 +2440,68 @@ async function main() {
2418
2440
  parentSessionId: isStdioWatcher ? watcherSessionId : null,
2419
2441
  });
2420
2442
  }
2421
- if (watcherShouldRun && subagentWatcherEnabled) {
2422
- activeSubagentWatcher = new SubagentWatcher({
2443
+ if (subagentWatcherEnabled) {
2444
+ // Base options are mode-independent. The scoped extras are a single
2445
+ // ternary branch, not two independent fields, because the options
2446
+ // type makes `costSelfCheck` without `parentSessionId` a compile
2447
+ // error: the self-check compares this process's whole-tracker
2448
+ // subagent total against a re-parse of ONE session, and those are
2449
+ // the same population only while the watcher is filtered to that
2450
+ // session.
2451
+ const subagentWatcherBase = {
2423
2452
  storagePath: config.storagePath,
2424
- parentSessionId: isStdioWatcher ? watcherSessionId : undefined,
2425
2453
  // Only meaningful when unfiltered (--local) — lets discoverFiles()
2426
2454
  // skip sessions that already have a live --stdio owner tailing them.
2427
2455
  localStore,
2428
- // Runtime cost-self-check: a drift > 5% surfaces as an
2429
- // `AiObservabilityHealth { event: 'cost_self_check' }` event. We
2430
- // compare like-with-like from two INDEPENDENT code paths so a
2431
- // regression in either is caught:
2432
- // - trackedUsd: subagent cost the live CostTracker accumulated from
2433
- // the onSubagentTurn feed (the headline/persisted path), and
2434
- // - groundTruthUsd: an independent re-parse of the same session's
2435
- // subagent transcripts via SubagentTimelineStore (the trace path).
2436
- // Both dedup streaming-duplicate lines by message.id, so a healthy
2437
- // system reads ~0%; any divergence (e.g. one path regressing on dedup
2438
- // or pricing) shows up as a real, non-zero delta. Only meaningful in
2439
- // --stdio mode, where the watcher is scoped to this one session.
2440
- costSelfCheck: () => {
2441
- const trackedUsd = costTracker.getSubagentMetrics().subagentUsd;
2442
- let groundTruthUsd = trackedUsd;
2443
- try {
2444
- const tl = subagentTimelineInstance.getSubagentsForSession(watcherSessionId);
2445
- groundTruthUsd = tl.agents.reduce((sum, a) => sum + (a.usd ?? 0), 0);
2446
- }
2447
- catch {
2448
- // On any re-parse error fall back to trackedUsd → 0% delta (no
2449
- // false alarm); the error is already surfaced via watcher health.
2450
- groundTruthUsd = trackedUsd;
2451
- }
2452
- return { trackedUsd, groundTruthUsd };
2453
- },
2454
- });
2456
+ };
2457
+ activeSubagentWatcher = new SubagentWatcher(isStdioWatcher
2458
+ ? {
2459
+ ...subagentWatcherBase,
2460
+ parentSessionId: watcherSessionId,
2461
+ // Runtime cost-self-check: a drift > 5% surfaces as an
2462
+ // `AiObservabilityHealth { event: 'cost_self_check' }` event.
2463
+ // We compare like-with-like from two INDEPENDENT code paths
2464
+ // so a regression in either is caught:
2465
+ // - trackedUsd: subagent cost the live CostTracker
2466
+ // accumulated from the onSubagentTurn feed (the
2467
+ // headline/persisted path), and
2468
+ // - groundTruthUsd: an independent re-parse of the same
2469
+ // session's subagent transcripts via SubagentTimelineStore
2470
+ // (the trace path).
2471
+ // Both dedup streaming-duplicate lines by message.id, so a
2472
+ // healthy system reads ~0%; any divergence (e.g. one path
2473
+ // regressing on dedup or pricing) shows up as a real, non-zero
2474
+ // delta. Only meaningful in --stdio mode, where the watcher is
2475
+ // scoped to this one session.
2476
+ costSelfCheck: () => {
2477
+ const trackedUsd = costTracker.getSubagentMetrics().subagentUsd;
2478
+ let groundTruthUsd = trackedUsd;
2479
+ try {
2480
+ const tl = subagentTimelineInstance.getSubagentsForSession(watcherSessionId);
2481
+ groundTruthUsd = tl.agents.reduce((sum, a) => sum + (a.usd ?? 0), 0);
2482
+ }
2483
+ catch {
2484
+ // On any re-parse error fall back to trackedUsd → 0% delta
2485
+ // (no false alarm); the error is already surfaced via
2486
+ // watcher health.
2487
+ groundTruthUsd = trackedUsd;
2488
+ }
2489
+ return { trackedUsd, groundTruthUsd };
2490
+ },
2491
+ }
2492
+ : subagentWatcherBase);
2455
2493
  activeSubagentWatcher.start();
2456
2494
  logger.info('SubagentWatcher started', {
2457
- mode: watcherMode,
2458
2495
  parentSessionId: isStdioWatcher ? watcherSessionId : null,
2459
2496
  });
2460
2497
  }
2461
- if (watcherShouldRun && workflowWatcherEnabled) {
2498
+ // Engine-only: unlike SubagentWatcher, WorkflowWatcher has no heartbeat
2499
+ // exclusion, so running it unfiltered in --local would reintroduce the
2500
+ // same race for workflow transcripts.
2501
+ if (isStdioWatcher && workflowWatcherEnabled) {
2462
2502
  activeWorkflowWatcher = new WorkflowWatcher({
2463
2503
  storagePath: config.storagePath,
2464
- parentSessionId: isStdioWatcher ? watcherSessionId : undefined,
2504
+ parentSessionId: watcherSessionId,
2465
2505
  getCostForRun: (runId) => costTracker.getCostForWorkflowRun(runId),
2466
2506
  });
2467
2507
  activeWorkflowWatcher.setOnRun((run) => {
@@ -2471,10 +2511,7 @@ async function main() {
2471
2511
  capturedNrIngest?.ingestObservabilityHealth(health);
2472
2512
  });
2473
2513
  activeWorkflowWatcher.start();
2474
- logger.info('WorkflowWatcher started', {
2475
- mode: watcherMode,
2476
- parentSessionId: isStdioWatcher ? watcherSessionId : null,
2477
- });
2514
+ logger.info('WorkflowWatcher started', { parentSessionId: watcherSessionId });
2478
2515
  }
2479
2516
  };
2480
2517
  // Re-point the watchers from a provisional `pending-<ts>` session id to the