@newrelic/preflight 1.50.7 → 1.50.9

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/event-processor.d.ts.map +1 -1
  7. package/dist/hooks/event-processor.js +2 -0
  8. package/dist/hooks/event-processor.js.map +1 -1
  9. package/dist/hooks/subagent-watcher.d.ts +42 -16
  10. package/dist/hooks/subagent-watcher.d.ts.map +1 -1
  11. package/dist/hooks/subagent-watcher.js +76 -13
  12. package/dist/hooks/subagent-watcher.js.map +1 -1
  13. package/dist/index.js +101 -72
  14. package/dist/index.js.map +1 -1
  15. package/dist/metrics/git-activity-recorder.d.ts.map +1 -1
  16. package/dist/metrics/git-activity-recorder.js +2 -1
  17. package/dist/metrics/git-activity-recorder.js.map +1 -1
  18. package/dist/metrics/git-workspace-identity.d.ts +16 -2
  19. package/dist/metrics/git-workspace-identity.d.ts.map +1 -1
  20. package/dist/metrics/git-workspace-identity.js +13 -0
  21. package/dist/metrics/git-workspace-identity.js.map +1 -1
  22. package/dist/metrics/git-workspace-report.d.ts +1 -1
  23. package/dist/metrics/git-workspace-report.d.ts.map +1 -1
  24. package/dist/metrics/git-workspace-report.js +7 -4
  25. package/dist/metrics/git-workspace-report.js.map +1 -1
  26. package/dist/metrics/git-workspace-reporter.d.ts +4 -3
  27. package/dist/metrics/git-workspace-reporter.d.ts.map +1 -1
  28. package/dist/metrics/git-workspace-reporter.js +38 -7
  29. package/dist/metrics/git-workspace-reporter.js.map +1 -1
  30. package/dist/metrics/local-session-aggregator.d.ts +55 -4
  31. package/dist/metrics/local-session-aggregator.d.ts.map +1 -1
  32. package/dist/metrics/local-session-aggregator.js +73 -8
  33. package/dist/metrics/local-session-aggregator.js.map +1 -1
  34. package/dist/storage/session-store.d.ts +32 -0
  35. package/dist/storage/session-store.d.ts.map +1 -1
  36. package/dist/storage/session-store.js +37 -2
  37. package/dist/storage/session-store.js.map +1 -1
  38. package/dist/storage/types.d.ts +3 -1
  39. package/dist/storage/types.d.ts.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 +1 -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,
@@ -1571,22 +1581,12 @@ async function main() {
1571
1581
  observabilityHealth: {
1572
1582
  getSnapshot: () => {
1573
1583
  // 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.
1584
+ // A null binding => watcher disabled (env flag off) => a zeroed
1585
+ // "disabled" snapshot. costSelfCheckDeltaPct stays null until
1586
+ // the 1h self-check is wired.
1577
1587
  const stats = activeSubagentWatcher?.getHealthStats();
1578
1588
  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';
1589
+ const watcherDisabledReason = watcherActive ? null : 'env_var';
1590
1590
  return {
1591
1591
  watcherActive,
1592
1592
  filesWatched: stats?.filesWatched ?? 0,
@@ -2112,6 +2112,22 @@ async function main() {
2112
2112
  // belong in the model breakdown too — recording them only in the cost
2113
2113
  // tracker left Model Usage blind to every subagent-only session.
2114
2114
  modelUsageTracker.recordUsage(turn.model, usage, breakdown.totalUsd);
2115
+ // The only line that makes an orphan session's subagent spend durable:
2116
+ // without it, an unscoped process's aggregator never learns about this
2117
+ // session's subagent cost and persistSession() has nothing to write.
2118
+ // `agentId` is the same discriminator costTracker.recordTokenUsage
2119
+ // splits parent-vs-subagent on above, so the two trackers can't drift.
2120
+ localSessionAggregator.recordTokenUsage(turn.parentSessionId, {
2121
+ timestamp: turn.timestampMs,
2122
+ costUsd: breakdown.totalUsd,
2123
+ model: turn.model,
2124
+ inputTokens: turn.inputTokens,
2125
+ outputTokens: turn.outputTokens,
2126
+ cacheReadTokens: turn.cacheReadTokens,
2127
+ cacheCreationTokens: turn.cacheCreationTokens,
2128
+ thinkingTokens: turn.reasoningTokens,
2129
+ agentId: turn.agentId,
2130
+ });
2115
2131
  // Pricing miss → usd:null on the wire; we recompute here so
2116
2132
  // the breakdown view distinguishes "0 because pricing absent" from
2117
2133
  // "0 because the turn truly had zero cost".
@@ -2326,13 +2342,7 @@ async function main() {
2326
2342
  }
2327
2343
  }, SESSION_PERSIST_INTERVAL_MS);
2328
2344
  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
2345
  const isStdioWatcher = options.stdio === true;
2333
- const isLocalWatcher = !isStdioWatcher;
2334
- const watcherShouldRun = (isStdioWatcher && (watcherMode === 'stdio' || watcherMode === '')) ||
2335
- (isLocalWatcher && watcherMode === 'local');
2336
2346
  // The SubagentWatcher is the ONLY thing that feeds per-agent (subagent)
2337
2347
  // token cost into the CostTracker (via onSubagentTurn → subagentCostUsd).
2338
2348
  // With it off, a session's persisted/headline cost silently excludes ALL
@@ -2343,13 +2353,18 @@ async function main() {
2343
2353
  // (parentSessionId filter), so it only ever attributes that session's own
2344
2354
  // subagents — parent tokens (onTokenEvent, parent transcript) and subagent
2345
2355
  // tokens (onSubagentTurn, subagent transcripts) are disjoint, so there is no
2346
- // double count. The WorkflowWatcher stays opt-in (NR_AI_ENABLE_WORKFLOW_WATCHER=1).
2356
+ // double count. Unfiltered (`--local`), the heartbeat exclusion
2357
+ // (subagent-watcher.ts's discoverFiles) is what keeps it from racing a live
2358
+ // `--stdio` session's own scoped watcher over the same cursor files — see
2359
+ // that module's doc comment. The WorkflowWatcher stays engine-only: it has
2360
+ // no heartbeat exclusion, so running it unfiltered would reintroduce that
2361
+ // exact race for workflow transcripts.
2347
2362
  const subagentWatcherEnabled = process.env['NR_AI_ENABLE_SUBAGENT_WATCHER'] !== '0';
2348
2363
  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.
2364
+ // Unlike WorkflowWatcher, ParentTranscriptWatcher is not gated
2365
+ // by --stdio-vs-`--local` at all — see startWatchers() below for why. Do
2366
+ // not "fix" this to match WorkflowWatcher; that would reintroduce the exact
2367
+ // regression this divergence avoids.
2353
2368
  const parentTranscriptWatcherEnabled = process.env['NR_AI_ENABLE_PARENT_TRANSCRIPT_WATCHER'] !== '0';
2354
2369
  // CopilotUsageWatcher is the Copilot analog of ParentTranscriptWatcher
2355
2370
  // (token-exact cost from VS Code's Copilot debug logs) and follows the
@@ -2375,15 +2390,14 @@ async function main() {
2375
2390
  // ParentTranscriptWatcher feeds parent-session token/cost tracking —
2376
2391
  // the primary cost signal, not a secondary one like subagent/workflow
2377
2392
  // 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
2393
+ // unconditionally in every mode with zero coupling to --stdio-vs-`--local`;
2394
+ // gating this behind isStdioWatcher the same way WorkflowWatcher is
2395
+ // gated would mean a standalone `--local` deployment (no --stdio
2396
+ // sibling) goes from "buggy but nonzero" parent-cost tracking to
2397
+ // "exactly zero" by default a real regression. So it always runs,
2398
+ // gated only by its own opt-out flag. Race-safety for "--stdio and
2399
+ // --local both alive for the same session" comes for free from the
2400
+ // same getActiveSessionIdsFromHeartbeats() exclusion SubagentWatcher's
2387
2401
  // unscoped discovery already uses.
2388
2402
  if (parentTranscriptWatcherEnabled) {
2389
2403
  activeParentTranscriptWatcher = new ParentTranscriptWatcher({
@@ -2418,50 +2432,68 @@ async function main() {
2418
2432
  parentSessionId: isStdioWatcher ? watcherSessionId : null,
2419
2433
  });
2420
2434
  }
2421
- if (watcherShouldRun && subagentWatcherEnabled) {
2422
- activeSubagentWatcher = new SubagentWatcher({
2435
+ if (subagentWatcherEnabled) {
2436
+ // Base options are mode-independent. The scoped extras are a single
2437
+ // ternary branch, not two independent fields, because the options
2438
+ // type makes `costSelfCheck` without `parentSessionId` a compile
2439
+ // error: the self-check compares this process's whole-tracker
2440
+ // subagent total against a re-parse of ONE session, and those are
2441
+ // the same population only while the watcher is filtered to that
2442
+ // session.
2443
+ const subagentWatcherBase = {
2423
2444
  storagePath: config.storagePath,
2424
- parentSessionId: isStdioWatcher ? watcherSessionId : undefined,
2425
2445
  // Only meaningful when unfiltered (--local) — lets discoverFiles()
2426
2446
  // skip sessions that already have a live --stdio owner tailing them.
2427
2447
  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
- });
2448
+ };
2449
+ activeSubagentWatcher = new SubagentWatcher(isStdioWatcher
2450
+ ? {
2451
+ ...subagentWatcherBase,
2452
+ parentSessionId: watcherSessionId,
2453
+ // Runtime cost-self-check: a drift > 5% surfaces as an
2454
+ // `AiObservabilityHealth { event: 'cost_self_check' }` event.
2455
+ // We compare like-with-like from two INDEPENDENT code paths
2456
+ // so a regression in either is caught:
2457
+ // - trackedUsd: subagent cost the live CostTracker
2458
+ // accumulated from the onSubagentTurn feed (the
2459
+ // headline/persisted path), and
2460
+ // - groundTruthUsd: an independent re-parse of the same
2461
+ // session's subagent transcripts via SubagentTimelineStore
2462
+ // (the trace path).
2463
+ // Both dedup streaming-duplicate lines by message.id, so a
2464
+ // healthy system reads ~0%; any divergence (e.g. one path
2465
+ // regressing on dedup or pricing) shows up as a real, non-zero
2466
+ // delta. Only meaningful in --stdio mode, where the watcher is
2467
+ // scoped to this one session.
2468
+ costSelfCheck: () => {
2469
+ const trackedUsd = costTracker.getSubagentMetrics().subagentUsd;
2470
+ let groundTruthUsd = trackedUsd;
2471
+ try {
2472
+ const tl = subagentTimelineInstance.getSubagentsForSession(watcherSessionId);
2473
+ groundTruthUsd = tl.agents.reduce((sum, a) => sum + (a.usd ?? 0), 0);
2474
+ }
2475
+ catch {
2476
+ // On any re-parse error fall back to trackedUsd → 0% delta
2477
+ // (no false alarm); the error is already surfaced via
2478
+ // watcher health.
2479
+ groundTruthUsd = trackedUsd;
2480
+ }
2481
+ return { trackedUsd, groundTruthUsd };
2482
+ },
2483
+ }
2484
+ : subagentWatcherBase);
2455
2485
  activeSubagentWatcher.start();
2456
2486
  logger.info('SubagentWatcher started', {
2457
- mode: watcherMode,
2458
2487
  parentSessionId: isStdioWatcher ? watcherSessionId : null,
2459
2488
  });
2460
2489
  }
2461
- if (watcherShouldRun && workflowWatcherEnabled) {
2490
+ // Engine-only: unlike SubagentWatcher, WorkflowWatcher has no heartbeat
2491
+ // exclusion, so running it unfiltered in --local would reintroduce the
2492
+ // same race for workflow transcripts.
2493
+ if (isStdioWatcher && workflowWatcherEnabled) {
2462
2494
  activeWorkflowWatcher = new WorkflowWatcher({
2463
2495
  storagePath: config.storagePath,
2464
- parentSessionId: isStdioWatcher ? watcherSessionId : undefined,
2496
+ parentSessionId: watcherSessionId,
2465
2497
  getCostForRun: (runId) => costTracker.getCostForWorkflowRun(runId),
2466
2498
  });
2467
2499
  activeWorkflowWatcher.setOnRun((run) => {
@@ -2471,10 +2503,7 @@ async function main() {
2471
2503
  capturedNrIngest?.ingestObservabilityHealth(health);
2472
2504
  });
2473
2505
  activeWorkflowWatcher.start();
2474
- logger.info('WorkflowWatcher started', {
2475
- mode: watcherMode,
2476
- parentSessionId: isStdioWatcher ? watcherSessionId : null,
2477
- });
2506
+ logger.info('WorkflowWatcher started', { parentSessionId: watcherSessionId });
2478
2507
  }
2479
2508
  };
2480
2509
  // Re-point the watchers from a provisional `pending-<ts>` session id to the