@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.
- package/dist/alerts/local-alert-rule.d.ts +2 -2
- package/dist/dashboard/routes/api-handler.d.ts +5 -9
- package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
- package/dist/dashboard/routes/api-handler.js +12 -18
- package/dist/dashboard/routes/api-handler.js.map +1 -1
- package/dist/hooks/subagent-watcher.d.ts +42 -16
- package/dist/hooks/subagent-watcher.d.ts.map +1 -1
- package/dist/hooks/subagent-watcher.js +76 -13
- package/dist/hooks/subagent-watcher.js.map +1 -1
- package/dist/index.js +113 -76
- package/dist/index.js.map +1 -1
- package/dist/metrics/git-activity-recorder.d.ts +6 -5
- package/dist/metrics/git-activity-recorder.d.ts.map +1 -1
- package/dist/metrics/git-activity-recorder.js +39 -70
- package/dist/metrics/git-activity-recorder.js.map +1 -1
- package/dist/metrics/git-efficiency-tracker.d.ts +0 -1
- package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -1
- package/dist/metrics/git-efficiency-tracker.js +23 -57
- package/dist/metrics/git-efficiency-tracker.js.map +1 -1
- package/dist/metrics/git-event-classifier.d.ts +28 -1
- package/dist/metrics/git-event-classifier.d.ts.map +1 -1
- package/dist/metrics/git-event-classifier.js +57 -2
- package/dist/metrics/git-event-classifier.js.map +1 -1
- package/dist/metrics/git-workspace-report.d.ts +24 -1
- package/dist/metrics/git-workspace-report.d.ts.map +1 -1
- package/dist/metrics/git-workspace-report.js +141 -10
- package/dist/metrics/git-workspace-report.js.map +1 -1
- package/dist/metrics/git-workspace-reporter.d.ts +12 -0
- package/dist/metrics/git-workspace-reporter.d.ts.map +1 -1
- package/dist/metrics/git-workspace-reporter.js +43 -3
- package/dist/metrics/git-workspace-reporter.js.map +1 -1
- package/dist/metrics/local-session-aggregator.d.ts +59 -4
- package/dist/metrics/local-session-aggregator.d.ts.map +1 -1
- package/dist/metrics/local-session-aggregator.js +121 -13
- package/dist/metrics/local-session-aggregator.js.map +1 -1
- package/dist/storage/session-store.d.ts +32 -0
- package/dist/storage/session-store.d.ts.map +1 -1
- package/dist/storage/session-store.js +37 -2
- package/dist/storage/session-store.js.map +1 -1
- package/dist/web/assets/{index-CV85Em1K.js → index-BrrBgs1G.js} +1 -1
- package/dist/web/index.html +1 -1
- 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
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
|
2350
|
-
// by
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
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
|
-
//
|
|
2380
|
-
//
|
|
2381
|
-
//
|
|
2382
|
-
//
|
|
2383
|
-
// by
|
|
2384
|
-
//
|
|
2385
|
-
//
|
|
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 (
|
|
2422
|
-
|
|
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
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
groundTruthUsd = trackedUsd;
|
|
2451
|
-
|
|
2452
|
-
|
|
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
|
-
|
|
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:
|
|
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
|