@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.
- 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/event-processor.d.ts.map +1 -1
- package/dist/hooks/event-processor.js +2 -0
- package/dist/hooks/event-processor.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 +101 -72
- package/dist/index.js.map +1 -1
- package/dist/metrics/git-activity-recorder.d.ts.map +1 -1
- package/dist/metrics/git-activity-recorder.js +2 -1
- package/dist/metrics/git-activity-recorder.js.map +1 -1
- package/dist/metrics/git-workspace-identity.d.ts +16 -2
- package/dist/metrics/git-workspace-identity.d.ts.map +1 -1
- package/dist/metrics/git-workspace-identity.js +13 -0
- package/dist/metrics/git-workspace-identity.js.map +1 -1
- package/dist/metrics/git-workspace-report.d.ts +1 -1
- package/dist/metrics/git-workspace-report.d.ts.map +1 -1
- package/dist/metrics/git-workspace-report.js +7 -4
- package/dist/metrics/git-workspace-report.js.map +1 -1
- package/dist/metrics/git-workspace-reporter.d.ts +4 -3
- package/dist/metrics/git-workspace-reporter.d.ts.map +1 -1
- package/dist/metrics/git-workspace-reporter.js +38 -7
- package/dist/metrics/git-workspace-reporter.js.map +1 -1
- package/dist/metrics/local-session-aggregator.d.ts +55 -4
- package/dist/metrics/local-session-aggregator.d.ts.map +1 -1
- package/dist/metrics/local-session-aggregator.js +73 -8
- 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/storage/types.d.ts +3 -1
- package/dist/storage/types.d.ts.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 +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
|
|
1575
|
-
//
|
|
1576
|
-
//
|
|
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
|
-
|
|
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.
|
|
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
|
|
2350
|
-
// by
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
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
|
-
//
|
|
2380
|
-
//
|
|
2381
|
-
//
|
|
2382
|
-
//
|
|
2383
|
-
// by
|
|
2384
|
-
//
|
|
2385
|
-
//
|
|
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 (
|
|
2422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|