@newrelic/preflight 1.15.6 → 1.16.0

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 (70) hide show
  1. package/dist/config.d.ts +3 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +3 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/dashboard/dashboard-server.d.ts.map +1 -1
  6. package/dist/dashboard/dashboard-server.js +15 -0
  7. package/dist/dashboard/dashboard-server.js.map +1 -1
  8. package/dist/dashboard/routes/api-handler.d.ts +26 -18
  9. package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
  10. package/dist/dashboard/routes/api-handler.js +23 -15
  11. package/dist/dashboard/routes/api-handler.js.map +1 -1
  12. package/dist/data/copilot-pricing/README.md +42 -0
  13. package/dist/data/copilot-pricing/pricing.json +54 -0
  14. package/dist/data/copilot-sdk-extension/extension.mjs +123 -0
  15. package/dist/deploy/data-paths.d.ts +6 -4
  16. package/dist/deploy/data-paths.d.ts.map +1 -1
  17. package/dist/deploy/data-paths.js +5 -3
  18. package/dist/deploy/data-paths.js.map +1 -1
  19. package/dist/hooks/collector-script.d.ts +12 -3
  20. package/dist/hooks/collector-script.d.ts.map +1 -1
  21. package/dist/hooks/collector-script.js +70 -18
  22. package/dist/hooks/collector-script.js.map +1 -1
  23. package/dist/hooks/copilot-sdk-usage-mapper.d.ts +45 -0
  24. package/dist/hooks/copilot-sdk-usage-mapper.d.ts.map +1 -0
  25. package/dist/hooks/copilot-sdk-usage-mapper.js +73 -0
  26. package/dist/hooks/copilot-sdk-usage-mapper.js.map +1 -0
  27. package/dist/hooks/copilot-usage-watcher.d.ts +109 -0
  28. package/dist/hooks/copilot-usage-watcher.d.ts.map +1 -0
  29. package/dist/hooks/copilot-usage-watcher.js +436 -0
  30. package/dist/hooks/copilot-usage-watcher.js.map +1 -0
  31. package/dist/index.d.ts +11 -11
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +162 -70
  34. package/dist/index.js.map +1 -1
  35. package/dist/metrics/copilot-pricing-overlay.d.ts +42 -0
  36. package/dist/metrics/copilot-pricing-overlay.d.ts.map +1 -0
  37. package/dist/metrics/copilot-pricing-overlay.js +97 -0
  38. package/dist/metrics/copilot-pricing-overlay.js.map +1 -0
  39. package/dist/metrics/git-efficiency-tracker.d.ts +12 -1
  40. package/dist/metrics/git-efficiency-tracker.d.ts.map +1 -1
  41. package/dist/metrics/git-efficiency-tracker.js +18 -3
  42. package/dist/metrics/git-efficiency-tracker.js.map +1 -1
  43. package/dist/metrics/local-session-aggregator.d.ts +164 -0
  44. package/dist/metrics/local-session-aggregator.d.ts.map +1 -0
  45. package/dist/metrics/local-session-aggregator.js +444 -0
  46. package/dist/metrics/local-session-aggregator.js.map +1 -0
  47. package/dist/platforms/copilot-adapter.d.ts +1 -1
  48. package/dist/platforms/copilot-adapter.d.ts.map +1 -1
  49. package/dist/platforms/copilot-adapter.js +64 -10
  50. package/dist/platforms/copilot-adapter.js.map +1 -1
  51. package/dist/platforms/copilot-sdk-adapter.d.ts +24 -0
  52. package/dist/platforms/copilot-sdk-adapter.d.ts.map +1 -0
  53. package/dist/platforms/copilot-sdk-adapter.js +173 -0
  54. package/dist/platforms/copilot-sdk-adapter.js.map +1 -0
  55. package/dist/platforms/index.d.ts +1 -0
  56. package/dist/platforms/index.d.ts.map +1 -1
  57. package/dist/platforms/index.js +1 -0
  58. package/dist/platforms/index.js.map +1 -1
  59. package/dist/platforms/platform-registry.d.ts.map +1 -1
  60. package/dist/platforms/platform-registry.js +13 -11
  61. package/dist/platforms/platform-registry.js.map +1 -1
  62. package/dist/storage/session-store.d.ts +9 -0
  63. package/dist/storage/session-store.d.ts.map +1 -1
  64. package/dist/storage/session-store.js +213 -20
  65. package/dist/storage/session-store.js.map +1 -1
  66. package/dist/web/assets/{index-BasTEtUs.js → index-Bwc4qaqj.js} +1 -1
  67. package/dist/web/assets/index-DwdSz9gz.css +2 -0
  68. package/dist/web/index.html +2 -2
  69. package/package.json +2 -2
  70. package/dist/web/assets/index-BI-2ELf2.css +0 -2
package/dist/index.js CHANGED
@@ -1,83 +1,86 @@
1
1
  #!/usr/bin/env node
2
2
  import 'dotenv/config';
3
+ import { Command } from 'commander';
3
4
  import { readFileSync, realpathSync } from 'node:fs';
4
5
  import { resolve } from 'node:path';
5
- import { Command } from 'commander';
6
- import { createLogger } from './shared/index.js';
7
- import { VERSION } from './version.js';
8
- import { checkNodeVersion } from './install/node-version-check.js';
9
- import { createServer } from './server.js';
10
- import { loadMcpConfig, DEFAULT_STORAGE_PATH } from './config.js';
11
- import { ProxyManager } from './proxy/index.js';
12
- import { LocalStore } from './storage/index.js';
13
- import { SessionStore, buildSessionSummary, sessionSummaryToDriftRecord, } from './storage/session-store.js';
14
- import { WeeklySummaryGenerator } from './storage/weekly-summary.js';
15
- import { purgeOldSessions, purgeOldWeeklySummaries } from './storage/retention.js';
6
+ import { AlertLog } from './alerts/alert-log.js';
7
+ import { AlertSnapshotCollector } from './alerts/alert-snapshot-collector.js';
8
+ import { LocalAlertEngine } from './alerts/local-alert-engine.js';
9
+ import { parseLocalAlertRules } from './alerts/local-alert-rule.js';
10
+ import { OsNotifier } from './alerts/os-notifier.js';
11
+ import { DEFAULT_STORAGE_PATH, loadMcpConfig } from './config.js';
12
+ import { DashboardServer } from './dashboard/dashboard-server.js';
13
+ import { LiveEventBus } from './dashboard/index.js';
14
+ import { SubagentTimelineStore } from './dashboard/subagent-timeline-store.js';
15
+ import { WorkflowStore } from './dashboard/workflow-store.js';
16
+ import { CopilotUsageWatcher } from './hooks/copilot-usage-watcher.js';
16
17
  import { HookEventProcessor } from './hooks/index.js';
17
- import { SessionTracker } from './metrics/session-tracker.js';
18
- import { CostTracker } from './metrics/cost-tracker.js';
19
- import { buildCostTrackerSeed } from './metrics/cost-tracker-seed.js';
20
- import { buildTaskDetectorSeed } from './metrics/task-detector-seed.js';
21
- import { buildCostForecastFromInputs } from './metrics/cost-forecast.js';
22
- import { BudgetTracker } from './metrics/budget-tracker.js';
23
- import { TaskDetector } from './metrics/task-detector.js';
18
+ import { ParentTranscriptWatcher } from './hooks/parent-transcript-watcher.js';
19
+ import { isSyntheticSessionId, resolveFromBreadcrumb, resolveFromCwd, resolveFromJobDir, resolveSessionId, watchPpidBreadcrumb, } from './hooks/session-resolver.js';
20
+ import { SubagentWatcher } from './hooks/subagent-watcher.js';
21
+ import { WorkflowWatcher } from './hooks/workflow-watcher.js';
22
+ import { migrateStoragePath } from './install/migrate.js';
23
+ import { checkNodeVersion } from './install/node-version-check.js';
24
+ import { localDateKey, todayPortionOfSessionCost } from './lib/date.js';
24
25
  import { AntiPatternDetector } from './metrics/anti-patterns.js';
25
- import { EfficiencyScorer } from './metrics/efficiency-score.js';
26
- import { TrendAnalyzer } from './metrics/trend-analyzer.js';
27
- import { CollaborationProfiler } from './metrics/collaboration-profile.js';
26
+ import { ApiFailureTracker } from './metrics/api-failure-tracker.js';
27
+ import { BudgetTracker } from './metrics/budget-tracker.js';
28
28
  import { ClaudeMdTracker } from './metrics/claudemd-tracker.js';
29
- import { createDefaultRegistry, GenericMcpAdapter } from './platforms/index.js';
29
+ import { CollaborationProfiler } from './metrics/collaboration-profile.js';
30
+ import { ContextCompositionTracker } from './metrics/context-composition-tracker.js';
31
+ import { ContextTrackerRegistry } from './metrics/context-tracker.js';
32
+ import { ContextWindowTracker } from './metrics/context-window-tracker.js';
33
+ import { applyCopilotPricingOverlay } from './metrics/copilot-pricing-overlay.js';
34
+ import { buildCostForecastFromInputs } from './metrics/cost-forecast.js';
30
35
  import { CostPerOutcomeAnalyzer } from './metrics/cost-per-outcome.js';
36
+ import { buildCostTrackerSeed } from './metrics/cost-tracker-seed.js';
37
+ import { CostTracker } from './metrics/cost-tracker.js';
38
+ import { DecisionTracker } from './metrics/decision-tracker.js';
39
+ import { EfficiencyScorer } from './metrics/efficiency-score.js';
40
+ import { GitEfficiencyTracker, parseDefaultBranchFromSymbolicRef, } from './metrics/git-efficiency-tracker.js';
41
+ import { InstructionDriftTracker } from './metrics/instruction-drift-tracker.js';
42
+ import { LatencyTracker } from './metrics/latency-tracker.js';
43
+ import { LiveSessionRegistry } from './metrics/live-session-registry.js';
44
+ import { collectCommitsAcrossRepos, LocalSessionAggregator, RepoNameResolver, resolveAuthorEmail, } from './metrics/local-session-aggregator.js';
45
+ import { ModelUsageTracker } from './metrics/model-usage-tracker.js';
31
46
  import { PersonalCoach } from './metrics/personal-coach.js';
32
47
  import { PromptFeedbackEngine } from './metrics/prompt-feedback.js';
48
+ import { QualityProxyTracker } from './metrics/quality-proxy-tracker.js';
33
49
  import { RecommendationEngine } from './metrics/recommendation-engine.js';
34
- import { ContextWindowTracker } from './metrics/context-window-tracker.js';
35
- import { LatencyTracker } from './metrics/latency-tracker.js';
36
- import { TaskCompletionTracker } from './metrics/task-completion-tracker.js';
37
- import { ModelUsageTracker } from './metrics/model-usage-tracker.js';
38
50
  import { RetryDetector } from './metrics/retry-detector.js';
39
- import { ContextCompositionTracker } from './metrics/context-composition-tracker.js';
40
- import { ContextTrackerRegistry } from './metrics/context-tracker.js';
41
- import { DecisionTracker } from './metrics/decision-tracker.js';
42
- import { InstructionDriftTracker } from './metrics/instruction-drift-tracker.js';
51
+ import { SessionTracker } from './metrics/session-tracker.js';
52
+ import { TaskCompletionTracker } from './metrics/task-completion-tracker.js';
53
+ import { TaskDetector } from './metrics/task-detector.js';
54
+ import { buildTaskDetectorSeed } from './metrics/task-detector-seed.js';
43
55
  import { ToolSelectionScorer } from './metrics/tool-selection-scorer.js';
44
- import { QualityProxyTracker } from './metrics/quality-proxy-tracker.js';
45
- import { ApiFailureTracker } from './metrics/api-failure-tracker.js';
46
- import { LiveSessionRegistry } from './metrics/live-session-registry.js';
56
+ import { TranscriptMessageTracker } from './metrics/transcript-message-tracker.js';
57
+ import { TrendAnalyzer } from './metrics/trend-analyzer.js';
47
58
  import { TurnCostAttributor } from './metrics/turn-cost-attributor.js';
48
59
  import { TurnTracker } from './metrics/turn-tracker.js';
49
- import { GitEfficiencyTracker, parseDefaultBranchFromSymbolicRef, } from './metrics/git-efficiency-tracker.js';
50
- import { TranscriptMessageTracker } from './metrics/transcript-message-tracker.js';
51
60
  import { WorkflowRunTracker } from './metrics/workflow-run-tracker.js';
52
- import { SubagentWatcher } from './hooks/subagent-watcher.js';
53
- import { WorkflowWatcher } from './hooks/workflow-watcher.js';
54
- import { ParentTranscriptWatcher } from './hooks/parent-transcript-watcher.js';
55
- import { WorkflowStore } from './dashboard/workflow-store.js';
56
- import { SubagentTimelineStore } from './dashboard/subagent-timeline-store.js';
57
- import { NrIngestManager } from './transport/nr-ingest.js';
61
+ import { createDefaultRegistry, GenericMcpAdapter } from './platforms/index.js';
62
+ import { ProxyManager } from './proxy/index.js';
58
63
  import { AuditTrailManager } from './security/audit-trail.js';
59
- import { LiveEventBus } from './dashboard/index.js';
60
- import { DashboardServer } from './dashboard/dashboard-server.js';
61
- import { LocalAlertEngine } from './alerts/local-alert-engine.js';
62
- import { AlertSnapshotCollector } from './alerts/alert-snapshot-collector.js';
63
- import { AlertLog } from './alerts/alert-log.js';
64
- import { OsNotifier } from './alerts/os-notifier.js';
65
- import { parseLocalAlertRules } from './alerts/local-alert-rule.js';
66
- import { localDateKey, todayPortionOfSessionCost } from './lib/date.js';
64
+ import { createServer } from './server.js';
65
+ import { createLogger } from './shared/index.js';
66
+ import { LocalStore } from './storage/index.js';
67
+ import { purgeOldSessions, purgeOldWeeklySummaries } from './storage/retention.js';
68
+ import { buildSessionSummary, SessionStore, sessionSummaryToDriftRecord, } from './storage/session-store.js';
69
+ import { WeeklySummaryGenerator } from './storage/weekly-summary.js';
70
+ import { registerPendingTools, registerTools } from './tools/session-stats.js';
67
71
  import { FeedbackCollector } from './tools/workflow-tools.js';
68
- import { registerTools, registerPendingTools } from './tools/session-stats.js';
69
- import { resolveSessionId, resolveFromJobDir, resolveFromBreadcrumb, resolveFromCwd, isSyntheticSessionId, watchPpidBreadcrumb, } from './hooks/session-resolver.js';
70
72
  import { initMcpTracer } from './tracing/mcp-tracer.js';
71
73
  import { SessionSpan } from './tracing/session-span.js';
72
74
  import { TaskSpanTracker } from './tracing/task-span-tracker.js';
73
75
  import { emitToolCallSpan } from './tracing/tool-call-span.js';
74
- import { migrateStoragePath } from './install/migrate.js';
75
- export { VERSION };
76
- export { NrMcpServer, createServer } from './server.js';
76
+ import { NrIngestManager } from './transport/nr-ingest.js';
77
+ import { VERSION } from './version.js';
77
78
  export { loadMcpConfig, redactSensitive } from './config.js';
78
- export { LocalStore } from './storage/index.js';
79
+ export { AmazonQAdapter, ClaudeCodeAdapter, ContinueAdapter, CopilotAdapter, createDefaultRegistry, CursorAdapter, GenericMcpAdapter, parseCopilotUsageResponse, PlatformRegistry, REPORT_SESSION_END_TOOL, REPORT_SESSION_START_TOOL, REPORT_TOOL_CALL_TOOL, validateReportToolCallInput, WindsurfAdapter, ZedAdapter, } from './platforms/index.js';
79
80
  export { ProxyManager } from './proxy/index.js';
80
- export { ClaudeCodeAdapter, CursorAdapter, WindsurfAdapter, CopilotAdapter, ZedAdapter, ContinueAdapter, AmazonQAdapter, parseCopilotUsageResponse, GenericMcpAdapter, validateReportToolCallInput, REPORT_TOOL_CALL_TOOL, REPORT_SESSION_START_TOOL, REPORT_SESSION_END_TOOL, PlatformRegistry, createDefaultRegistry, } from './platforms/index.js';
81
+ export { createServer, NrMcpServer } from './server.js';
82
+ export { LocalStore } from './storage/index.js';
83
+ export { VERSION };
81
84
  const logger = createLogger('mcp-cli');
82
85
  // Show first-4 and last-4 chars of a credential. Guards against short values
83
86
  // (e.g. test stubs) that would otherwise expose the full secret.
@@ -327,6 +330,7 @@ const SUBCOMMAND_NAMES = [
327
330
  'update',
328
331
  'schedule',
329
332
  'doctor',
333
+ 'local',
330
334
  ];
331
335
  // Install-CLI subcommands — a subset of SUBCOMMAND_NAMES routed to runInstallCli.
332
336
  // Derived from SUBCOMMAND_NAMES to ensure a single source of truth: deploy-*
@@ -542,6 +546,7 @@ async function main() {
542
546
  let activeSubagentWatcher = null;
543
547
  let activeWorkflowWatcher = null;
544
548
  let activeParentTranscriptWatcher = null;
549
+ let activeCopilotUsageWatcher = null;
545
550
  // Aborts the async resolveSessionId polling loop when shutdown fires so
546
551
  // the breadcrumb poll does not outlive the process.
547
552
  let sessionResolutionAbort;
@@ -634,6 +639,7 @@ async function main() {
634
639
  activeSubagentWatcher?.stop();
635
640
  activeWorkflowWatcher?.stop();
636
641
  activeParentTranscriptWatcher?.stop();
642
+ activeCopilotUsageWatcher?.stop();
637
643
  liveSessionRegistry?.stopSampling();
638
644
  // Use allSettled so a failure in one stop() doesn't prevent the others.
639
645
  const stopResults = await Promise.allSettled([
@@ -696,6 +702,7 @@ async function main() {
696
702
  await mcpServer.close();
697
703
  process.exit(0);
698
704
  }
705
+ applyCopilotPricingOverlay(config.customPricingFile);
699
706
  const fromJobDir = resolveFromJobDir(process.env.CLAUDE_JOB_DIR ?? null);
700
707
  const fromPpid = fromJobDir ? null : resolveFromBreadcrumb(config.storagePath, process.ppid);
701
708
  const fromCwd = fromJobDir || fromPpid ? null : resolveFromCwd(config.storagePath, process.cwd());
@@ -736,6 +743,7 @@ async function main() {
736
743
  logger.info('Server disabled via config — exiting');
737
744
  process.exit(0);
738
745
  }
746
+ applyCopilotPricingOverlay(config.customPricingFile);
739
747
  // --local has no owning Claude Code session — derive a deterministic
740
748
  // identifier so the rest of the codebase can rely on a non-empty
741
749
  // sessionTraceId without fabricating a UUID.
@@ -862,6 +870,13 @@ async function main() {
862
870
  sessionStore = new SessionStore({ storagePath: config.storagePath });
863
871
  const currentSessionId = sessionTracker.getMetrics().sessionId;
864
872
  let currentRepoName = null;
873
+ // An unscoped process (--local, or a provisional --stdio window) drains
874
+ // every unowned buffer and folds it into trackers keyed by its own
875
+ // synthetic id, which persistSession() skips — so without this rollup the
876
+ // sessions it observes are never written to disk at all. See
877
+ // local-session-aggregator.ts for why that hits Copilot but not Claude Code.
878
+ const localSessionAggregator = new LocalSessionAggregator();
879
+ const repoNameResolver = new RepoNameResolver();
865
880
  const budgetTracker = new BudgetTracker({
866
881
  sessionBudgetUsd: config.sessionBudgetUsd,
867
882
  dailyBudgetUsd: config.dailyBudgetUsd,
@@ -992,19 +1007,45 @@ async function main() {
992
1007
  // repo history to get an accurate commit count for today.
993
1008
  // spawnSync with ENOENT doesn't throw — it returns { status: null, error: Error }.
994
1009
  // The status === 0 guard handles unavailable-git without a try/catch.
995
- const todayStr = new Date().toISOString().slice(0, 10);
996
- const logResult = spawnSync('git', ['log', `--since=${todayStr}T00:00:00Z`, '--format=%H %ct'], GIT_OPTS);
997
- if (logResult.status === 0 && logResult.stdout !== null) {
998
- const commits = logResult.stdout
999
- .trim()
1000
- .split('\n')
1001
- .filter(Boolean)
1002
- .map((line) => {
1003
- const [hash, epochStr] = line.split(' ');
1004
- return { hash: hash ?? '', timestamp: parseInt(epochStr ?? '0', 10) * 1000 };
1005
- });
1006
- gitEfficiencyTracker.hydrateGitLog(commits);
1007
- }
1010
+ //
1011
+ // Repos are collected from an explicit NR_AI_GIT_REPOS list plus every repo
1012
+ // root seen in observed session cwds, rather than relying on this process's
1013
+ // own cwd. A dashboard started in one repo would otherwise report that
1014
+ // repo's history no matter which repos are actually being worked in — and,
1015
+ // with no --author filter, count other contributors' commits as the user's.
1016
+ const gitAuthorEmail = resolveAuthorEmail(process.cwd());
1017
+ const configuredRepoRoots = (process.env.NR_AI_GIT_REPOS ?? '')
1018
+ .split(/[,:]/)
1019
+ .map((entry) => entry.trim())
1020
+ .filter(Boolean);
1021
+ const collectRepoRoots = () => {
1022
+ const roots = new Set();
1023
+ const selfRoot = repoNameResolver.root(process.cwd());
1024
+ if (selfRoot)
1025
+ roots.add(selfRoot);
1026
+ for (const dir of configuredRepoRoots) {
1027
+ const root = repoNameResolver.root(dir);
1028
+ if (root)
1029
+ roots.add(root);
1030
+ }
1031
+ for (const cwd of localSessionAggregator.cwds()) {
1032
+ const root = repoNameResolver.root(cwd);
1033
+ if (root)
1034
+ roots.add(root);
1035
+ }
1036
+ return [...roots];
1037
+ };
1038
+ const hydrateGitCommits = () => {
1039
+ // Recomputed per call so a long-lived dashboard rolls over at midnight
1040
+ // instead of reporting "today" relative to the day it was started.
1041
+ const since = new Date().toISOString().slice(0, 10);
1042
+ const commits = collectCommitsAcrossRepos(collectRepoRoots(), since, gitAuthorEmail);
1043
+ if (commits.length > 0)
1044
+ gitEfficiencyTracker.hydrateGitLog(commits);
1045
+ };
1046
+ hydrateGitCommits();
1047
+ const gitHydrationInterval = setInterval(hydrateGitCommits, 5 * 60_000);
1048
+ gitHydrationInterval.unref();
1008
1049
  // Branch divergence from the real default branch — how far ahead/behind
1009
1050
  // are we? The dashboard polls this every 5s, implying a live number, but
1010
1051
  // without this re-hydration it would otherwise be computed once here at
@@ -1314,6 +1355,7 @@ async function main() {
1314
1355
  watcherDisabledByLock: stats?.watcherDisabledByLock ?? false,
1315
1356
  costSelfCheckDeltaPct: null,
1316
1357
  watcherDisabledReason,
1358
+ copilotDebugLoggingDisabled: activeCopilotUsageWatcher?.getHealth().debugLoggingLikelyDisabled ?? false,
1317
1359
  };
1318
1360
  },
1319
1361
  },
@@ -1462,6 +1504,7 @@ async function main() {
1462
1504
  const taskIdBeforeRecord = config.otlp.transport !== 'nr-events-api' ? taskDetector.getActiveTaskId() : null;
1463
1505
  sessionTracker.recordToolCall(rawRecord);
1464
1506
  taskDetector.recordToolCall(rawRecord);
1507
+ localSessionAggregator.recordToolCall(rawRecord);
1465
1508
  if (rawRecord.sessionId) {
1466
1509
  liveSessionRegistry.touch(rawRecord.sessionId, rawRecord.cwd);
1467
1510
  }
@@ -1660,6 +1703,15 @@ async function main() {
1660
1703
  timestampMs: tokenEvent.timestamp,
1661
1704
  });
1662
1705
  modelUsageTracker.recordUsage(tokenEvent.model, tokenEvent.inputTokens, tokenEvent.outputTokens, breakdown.totalUsd);
1706
+ localSessionAggregator.recordTokenUsage(tokenEvent.sessionId, {
1707
+ timestamp: tokenEvent.timestamp,
1708
+ costUsd: breakdown.totalUsd,
1709
+ model: tokenEvent.model,
1710
+ inputTokens: tokenEvent.inputTokens,
1711
+ outputTokens: tokenEvent.outputTokens,
1712
+ cacheReadTokens: tokenEvent.cacheReadTokens,
1713
+ cacheCreationTokens: tokenEvent.cacheCreationTokens,
1714
+ });
1663
1715
  contextCompositionTracker.recordTokenEvent(tokenEvent);
1664
1716
  const ctxSnapshot = contextTracker.recordTurn(tokenEvent);
1665
1717
  if (ctxSnapshot && tokenEvent.sessionId) {
@@ -1754,6 +1806,10 @@ async function main() {
1754
1806
  workflowRunId: turn.workflowRunId,
1755
1807
  agentId: turn.agentId,
1756
1808
  });
1809
+ // Subagent turns are real model requests and cost real money, so they
1810
+ // belong in the model breakdown too — recording them only in the cost
1811
+ // tracker left Model Usage blind to every subagent-only session.
1812
+ modelUsageTracker.recordUsage(turn.model, turn.inputTokens, turn.outputTokens, breakdown.totalUsd);
1757
1813
  // Pricing miss → usd:null on the wire; we recompute here so
1758
1814
  // the breakdown view distinguishes "0 because pricing absent" from
1759
1815
  // "0 because the turn truly had zero cost".
@@ -1832,9 +1888,24 @@ async function main() {
1832
1888
  // real session id is still being resolved).
1833
1889
  const isSyntheticId = isSyntheticSessionId(summary.sessionId);
1834
1890
  if (isSyntheticId) {
1891
+ // This process owns no real session of its own, but it has been
1892
+ // draining other sessions' buffers destructively — so if it returns
1893
+ // here without writing them, those events are simply lost. Persist
1894
+ // each real session it observed instead.
1895
+ const rollups = localSessionAggregator.toSummaries({
1896
+ developer: config.developer ?? 'unknown',
1897
+ platform: eventProcessor?.activePlatform,
1898
+ outcome: opts?.periodic ? 'in progress' : 'completed',
1899
+ toolSelectionScorer,
1900
+ repoResolver: repoNameResolver,
1901
+ });
1902
+ for (const rollup of rollups) {
1903
+ sessionStore.saveSession(rollup);
1904
+ }
1835
1905
  if (!opts?.periodic) {
1836
- logger.info('Skipping synthetic session JSON persistence', {
1906
+ logger.info('Persisted observed sessions on behalf of synthetic owner', {
1837
1907
  sessionId: summary.sessionId,
1908
+ persistedSessions: rollups.length,
1838
1909
  });
1839
1910
  }
1840
1911
  return;
@@ -1905,6 +1976,12 @@ async function main() {
1905
1976
  // why. Do not "fix" this to match the other two; that would reintroduce
1906
1977
  // the exact regression this divergence avoids.
1907
1978
  const parentTranscriptWatcherEnabled = process.env['NR_AI_ENABLE_PARENT_TRANSCRIPT_WATCHER'] !== '0';
1979
+ // CopilotUsageWatcher is the Copilot analog of ParentTranscriptWatcher
1980
+ // (token-exact cost from VS Code's Copilot debug logs) and follows the
1981
+ // same always-run rationale: it feeds the primary cost signal, so it is
1982
+ // gated only by its own opt-out. It no-ops cheaply when no VS Code
1983
+ // workspaceStorage dirs exist.
1984
+ const copilotUsageWatcherEnabled = process.env['NR_AI_ENABLE_COPILOT_USAGE_WATCHER'] !== '0';
1908
1985
  // Construct + start the watchers for a given session id. In `--stdio` mode
1909
1986
  // the watchers filter discovered transcript dirs by `parentSessionId`; in
1910
1987
  // `--local` mode they run unfiltered (parentSessionId: undefined). Shared by
@@ -1936,6 +2013,17 @@ async function main() {
1936
2013
  parentSessionId: isStdioWatcher ? watcherSessionId : null,
1937
2014
  });
1938
2015
  }
2016
+ if (copilotUsageWatcherEnabled) {
2017
+ activeCopilotUsageWatcher = new CopilotUsageWatcher({
2018
+ storagePath: config.storagePath,
2019
+ parentSessionId: isStdioWatcher ? watcherSessionId : undefined,
2020
+ localStore,
2021
+ });
2022
+ activeCopilotUsageWatcher.start();
2023
+ logger.info('CopilotUsageWatcher started', {
2024
+ parentSessionId: isStdioWatcher ? watcherSessionId : null,
2025
+ });
2026
+ }
1939
2027
  if (watcherShouldRun && subagentWatcherEnabled) {
1940
2028
  activeSubagentWatcher = new SubagentWatcher({
1941
2029
  storagePath: config.storagePath,
@@ -2006,6 +2094,10 @@ async function main() {
2006
2094
  activeParentTranscriptWatcher.stop();
2007
2095
  activeParentTranscriptWatcher = null;
2008
2096
  }
2097
+ if (activeCopilotUsageWatcher) {
2098
+ activeCopilotUsageWatcher.stop();
2099
+ activeCopilotUsageWatcher = null;
2100
+ }
2009
2101
  if (activeSubagentWatcher) {
2010
2102
  activeSubagentWatcher.stop();
2011
2103
  activeSubagentWatcher = null;