@newrelic/preflight 1.68.0 → 1.69.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 (118) hide show
  1. package/README.md +1 -1
  2. package/dist/config.d.ts +18 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +76 -6
  5. package/dist/config.js.map +1 -1
  6. package/dist/dashboard/routes/api-handler.d.ts +1 -0
  7. package/dist/dashboard/routes/api-handler.d.ts.map +1 -1
  8. package/dist/dashboard/routes/api-handler.js +109 -91
  9. package/dist/dashboard/routes/api-handler.js.map +1 -1
  10. package/dist/data/dashboards/ai-coding-assistant-adoption-cost.json +291 -1
  11. package/dist/data/dashboards/ai-coding-assistant-team-view.json +6 -6
  12. package/dist/hooks/billed-turn-ledger.d.ts +46 -0
  13. package/dist/hooks/billed-turn-ledger.d.ts.map +1 -0
  14. package/dist/hooks/billed-turn-ledger.js +274 -0
  15. package/dist/hooks/billed-turn-ledger.js.map +1 -0
  16. package/dist/hooks/collector-script.d.ts.map +1 -1
  17. package/dist/hooks/collector-script.js +5 -1
  18. package/dist/hooks/collector-script.js.map +1 -1
  19. package/dist/hooks/event-processor.d.ts +3 -7
  20. package/dist/hooks/event-processor.d.ts.map +1 -1
  21. package/dist/hooks/event-processor.js +21 -7
  22. package/dist/hooks/event-processor.js.map +1 -1
  23. package/dist/hooks/parent-transcript-watcher.d.ts +6 -5
  24. package/dist/hooks/parent-transcript-watcher.d.ts.map +1 -1
  25. package/dist/hooks/parent-transcript-watcher.js +64 -62
  26. package/dist/hooks/parent-transcript-watcher.js.map +1 -1
  27. package/dist/hooks/throttled-warning.d.ts +16 -0
  28. package/dist/hooks/throttled-warning.d.ts.map +1 -0
  29. package/dist/hooks/throttled-warning.js +32 -0
  30. package/dist/hooks/throttled-warning.js.map +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +26 -8
  33. package/dist/index.js.map +1 -1
  34. package/dist/install/cli.d.ts.map +1 -1
  35. package/dist/install/cli.js +53 -0
  36. package/dist/install/cli.js.map +1 -1
  37. package/dist/install/diagnostics.d.ts.map +1 -1
  38. package/dist/install/diagnostics.js +17 -3
  39. package/dist/install/diagnostics.js.map +1 -1
  40. package/dist/install/keychain.d.ts +18 -0
  41. package/dist/install/keychain.d.ts.map +1 -0
  42. package/dist/install/keychain.js +60 -0
  43. package/dist/install/keychain.js.map +1 -0
  44. package/dist/install/kiro-install-helper.d.ts +27 -0
  45. package/dist/install/kiro-install-helper.d.ts.map +1 -0
  46. package/dist/install/kiro-install-helper.js +53 -0
  47. package/dist/install/kiro-install-helper.js.map +1 -0
  48. package/dist/install/setup-wizard.d.ts +4 -0
  49. package/dist/install/setup-wizard.d.ts.map +1 -1
  50. package/dist/install/setup-wizard.js +94 -9
  51. package/dist/install/setup-wizard.js.map +1 -1
  52. package/dist/metrics/cost-tracker-seed.d.ts +16 -0
  53. package/dist/metrics/cost-tracker-seed.d.ts.map +1 -1
  54. package/dist/metrics/cost-tracker-seed.js +42 -7
  55. package/dist/metrics/cost-tracker-seed.js.map +1 -1
  56. package/dist/metrics/cost-tracker.d.ts +11 -3
  57. package/dist/metrics/cost-tracker.d.ts.map +1 -1
  58. package/dist/metrics/cost-tracker.js +4 -1
  59. package/dist/metrics/cost-tracker.js.map +1 -1
  60. package/dist/metrics/git-activity-recorder.d.ts +3 -3
  61. package/dist/metrics/git-activity-recorder.d.ts.map +1 -1
  62. package/dist/metrics/git-activity-recorder.js +3 -3
  63. package/dist/metrics/git-activity-recorder.js.map +1 -1
  64. package/dist/metrics/git-workspace-identity.d.ts +6 -1
  65. package/dist/metrics/git-workspace-identity.d.ts.map +1 -1
  66. package/dist/metrics/git-workspace-identity.js +56 -32
  67. package/dist/metrics/git-workspace-identity.js.map +1 -1
  68. package/dist/metrics/git-workspace-reporter.d.ts +4 -3
  69. package/dist/metrics/git-workspace-reporter.d.ts.map +1 -1
  70. package/dist/metrics/git-workspace-reporter.js +4 -3
  71. package/dist/metrics/git-workspace-reporter.js.map +1 -1
  72. package/dist/metrics/skill-activation-tracker.d.ts +27 -0
  73. package/dist/metrics/skill-activation-tracker.d.ts.map +1 -0
  74. package/dist/metrics/skill-activation-tracker.js +92 -0
  75. package/dist/metrics/skill-activation-tracker.js.map +1 -0
  76. package/dist/metrics/turn-cost-attributor.d.ts +34 -16
  77. package/dist/metrics/turn-cost-attributor.d.ts.map +1 -1
  78. package/dist/metrics/turn-cost-attributor.js +95 -60
  79. package/dist/metrics/turn-cost-attributor.js.map +1 -1
  80. package/dist/secret-command.d.ts +5 -3
  81. package/dist/secret-command.d.ts.map +1 -1
  82. package/dist/secret-command.js +17 -11
  83. package/dist/secret-command.js.map +1 -1
  84. package/dist/shared/pricing-data.d.ts.map +1 -1
  85. package/dist/shared/pricing-data.js +223 -32
  86. package/dist/shared/pricing-data.js.map +1 -1
  87. package/dist/shared/pricing.d.ts +25 -3
  88. package/dist/shared/pricing.d.ts.map +1 -1
  89. package/dist/shared/pricing.js +46 -9
  90. package/dist/shared/pricing.js.map +1 -1
  91. package/dist/shared/tokens.d.ts +16 -0
  92. package/dist/shared/tokens.d.ts.map +1 -1
  93. package/dist/shared/tokens.js +18 -0
  94. package/dist/shared/tokens.js.map +1 -1
  95. package/dist/shared/transport/otlp-shared.d.ts.map +1 -1
  96. package/dist/shared/transport/otlp-shared.js +7 -14
  97. package/dist/shared/transport/otlp-shared.js.map +1 -1
  98. package/dist/storage/session-store.d.ts +24 -1
  99. package/dist/storage/session-store.d.ts.map +1 -1
  100. package/dist/storage/session-store.js +34 -4
  101. package/dist/storage/session-store.js.map +1 -1
  102. package/dist/storage/types.d.ts +13 -0
  103. package/dist/storage/types.d.ts.map +1 -1
  104. package/dist/storage/types.js.map +1 -1
  105. package/dist/tools/session-stats.js +1 -1
  106. package/dist/tools/session-stats.js.map +1 -1
  107. package/dist/transport/nr-ingest.d.ts +4 -0
  108. package/dist/transport/nr-ingest.d.ts.map +1 -1
  109. package/dist/transport/nr-ingest.js +43 -19
  110. package/dist/transport/nr-ingest.js.map +1 -1
  111. package/dist/transport/tier-types.d.ts +11 -1
  112. package/dist/transport/tier-types.d.ts.map +1 -1
  113. package/dist/transport/tier-types.js +28 -6
  114. package/dist/transport/tier-types.js.map +1 -1
  115. package/dist/web/assets/{index-lvdXUntr.js → index-U9DPZ-Tb.js} +3 -3
  116. package/dist/web/index.html +1 -1
  117. package/examples/local-alert-rules.json +28 -0
  118. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
- import { digestWebhookUrlOverride, normalizeDeveloperName, redactSensitive } from '../../config.js';
2
+ import { digestWebhookUrlOverrideSource, digestWebhookUrlOverride, normalizeDeveloperName, redactSensitive, } from '../../config.js';
3
3
  import { isSyntheticSessionId, isUnscopedAggregatorSessionId, } from '../../hooks/session-resolver.js';
4
- import { localDateKey, localStartOfDay, todayPortionOfSessionCost, todayPortionRatio, } from '../../lib/date.js';
4
+ import { localDateKey, localStartOfDay, todayPortionRatio } from '../../lib/date.js';
5
+ import { todayCostOfSummary } from '../../metrics/cost-tracker-seed.js';
5
6
  import { AntiPatternDetector } from '../../metrics/anti-patterns.js';
6
7
  import { computeContextMetricsFromEvents } from '../../metrics/context-tracker.js';
7
8
  import { sumCallUsageInto } from '../../metrics/call-usage.js';
@@ -17,9 +18,9 @@ import { combineQualityProxyRawCounts, QualityProxyTracker, ZERO_QUALITY_PROXY_C
17
18
  import { deriveSessionStatus, SESSION_STATUSES, } from '../../metrics/session-status.js';
18
19
  import { toToolSelectionSummary } from '../../metrics/tool-selection-scorer.js';
19
20
  import { computeUsageInsights } from '../../metrics/usage-insights.js';
20
- import { hasAttributableActivity, toPersistedAntiPatterns } from '../../storage/session-store.js';
21
+ import { toPersistedAntiPatterns } from '../../storage/session-store.js';
21
22
  import { getIsoWeekId } from '../../storage/weekly-summary.js';
22
- import { digestWebhookUrlShadowedBy, handleSendDigest } from '../../tools/cross-session-tools.js';
23
+ import { digestWebhookUrlShadowedBy, handleSendDigest, isDigestWebhookCommandConfigured, } from '../../tools/cross-session-tools.js';
23
24
  import { computeCacheHealth } from './cache-health-aggregate.js';
24
25
  import { computeLatencyPercentiles } from './latency-percentiles.js';
25
26
  import { analyzeReplayTimeline } from './replay-analyzer.js';
@@ -207,14 +208,15 @@ function mergeToolTypeCostEntry(existing, bucket) {
207
208
  */
208
209
  function mergeSkillCostEntry(existing, bucket) {
209
210
  const totalCost = (existing?.totalCost ?? 0) + bucket.costUsd;
211
+ // A persisted skill bucket's `count` is runs, not tool calls, and it keeps
212
+ // no attributed-call count, so it adds runs only; avgCost is per run on
213
+ // every path, matching SkillCostEntry.
210
214
  const callCount = (existing?.callCount ?? 0) + bucket.count;
211
- // A persisted bucket only ever holds attributed cost, so every call it counts is attributed.
212
- const attributedCallCount = (existing?.attributedCallCount ?? 0) + bucket.count;
213
215
  return {
214
216
  callCount,
215
- attributedCallCount,
217
+ attributedCallCount: existing?.attributedCallCount ?? 0,
216
218
  totalCost,
217
- avgCost: attributedCallCount > 0 ? totalCost / attributedCallCount : 0,
219
+ avgCost: callCount > 0 ? totalCost / callCount : 0,
218
220
  inputTokens: (existing?.inputTokens ?? 0) + (bucket.breakdown?.inputTokens ?? 0),
219
221
  outputTokens: (existing?.outputTokens ?? 0) + (bucket.breakdown?.outputTokens ?? 0),
220
222
  cacheReadTokens: (existing?.cacheReadTokens ?? 0) + (bucket.breakdown?.cacheReadTokens ?? 0),
@@ -789,7 +791,7 @@ function computeSessionStatusAggregate(input) {
789
791
  trackLatestTool(session.sessionId, entry.toolName, entry.timestamp);
790
792
  }
791
793
  }
792
- const identityResolver = new WorktreeIdentityResolver();
794
+ const { identityResolver } = input;
793
795
  const activityStore = new ActivityStore();
794
796
  const activityRecorder = new GitActivityRecorder(activityStore, identityResolver);
795
797
  const bufferToolCalls = pairToolCallsFromBufferEvents(peeked).filter((record) => record.timestamp >= startMs);
@@ -846,6 +848,9 @@ export function createApiHandler(deps) {
846
848
  // its identity resolution makes — is only ever paid once across every
847
849
  // future /api/git-efficiency request, not on every poll.
848
850
  const gitReplayCache = new ReplaySessionCache();
851
+ // Shared so each repo's identity (several `git` spawns) is resolved once per
852
+ // 30s TTL instead of once per request.
853
+ const gitIdentityResolver = new WorktreeIdentityResolver();
849
854
  routes.set('GET /api/session/current', (_req, res) => {
850
855
  if (!deps.sessionTracker)
851
856
  return unavailable(res, 'sessionTracker');
@@ -1227,14 +1232,14 @@ export function createApiHandler(deps) {
1227
1232
  // (costByDayUsd) let us sum a session's REAL today-spend instead of
1228
1233
  // pro-rating its lifetime estimatedCostUsd by a timeline.
1229
1234
  const todayKey = localDateKey(now);
1235
+ let liveCheckpointTodayUsd = 0;
1236
+ let liveCheckpointSubagentUsd = 0;
1237
+ let liveCheckpointCacheSavingsUsd = 0;
1238
+ let liveCheckpointCacheReadTokens = 0;
1239
+ let liveCheckpointCacheCreationTokens = 0;
1240
+ let liveCheckpointInputTokens = 0;
1230
1241
  for (const raw of overlappingTodaySessions) {
1231
1242
  const s = raw;
1232
- // Skip the live session here — its today-portion is added below from
1233
- // costTracker.getCostForDay()/getSubagentCostForDay(), which is more
1234
- // accurate (per-token-event) than pro-rating from a periodically-
1235
- // persisted snapshot.
1236
- if (s.sessionId === liveSid)
1237
- continue;
1238
1243
  // Cross-midnight sessions that started before today aren't covered by
1239
1244
  // the todaySessions timeline walk above (loadTodaySessions() filters
1240
1245
  // by file-date = start date, so a session that started yesterday never
@@ -1250,43 +1255,35 @@ export function createApiHandler(deps) {
1250
1255
  }
1251
1256
  }
1252
1257
  }
1253
- // Cost attributed to TODAY. Prefer the session's persisted per-day
1254
- // bucket — authoritative, since each token event was bucketed by its real
1255
- // transcript timestamp. For older session files without buckets, fall back
1256
- // to pro-rating the lifetime estimatedCostUsd by the timeline — EXCEPT a
1257
- // session with no attributable activity (see hasAttributableActivity's doc
1258
- // comment) is an unverifiable re-read artifact: its estimatedCostUsd is a
1259
- // cumulative lifetime total that may include a resumed transcript's month
1260
- // of cache-read tokens re-read in one pass, and todayPortionRatio returns
1261
- // 1.0 for its entirely-today window, dumping the whole total onto today
1262
- // (the observed $248/$863 phantoms). Bias toward trust and contribute 0;
1263
- // the real per-day figure is recovered once the session re-persists WITH
1264
- // day buckets.
1258
+ // Cost attributed to TODAY: the persisted per-day bucket, else a guarded
1259
+ // pro-rate (see todayCostOfSummary). The same helper seeds CostTracker, so
1260
+ // a restart cannot change what a checkpoint contributes.
1261
+ //
1262
+ // The live session is held back: step (3) adds it as the larger of this
1263
+ // checkpoint figure and the live CostTracker's, so a tracker that was never
1264
+ // seeded (cwd-only adoption defers seeding, rehydrate can throw) cannot
1265
+ // drop the checkpoint's today cost, and a seeded one is not counted twice.
1266
+ // This is per-key max, the same rule mergeSummaries uses.
1267
+ const todayCost = todayCostOfSummary(s, todayKey, now);
1268
+ if (s.sessionId === liveSid) {
1269
+ liveCheckpointTodayUsd = todayCost.costUsd;
1270
+ liveCheckpointSubagentUsd = todayCost.subagentCostUsd;
1271
+ const liveRatio = todayPortionRatio(s, now);
1272
+ liveCheckpointCacheSavingsUsd = (s.cacheSavingsUsd ?? 0) * liveRatio;
1273
+ liveCheckpointCacheReadTokens = (s.tokensCacheRead ?? 0) * liveRatio;
1274
+ liveCheckpointCacheCreationTokens = (s.tokensCacheCreation ?? 0) * liveRatio;
1275
+ liveCheckpointInputTokens = (s.tokensInput ?? 0) * liveRatio;
1276
+ continue;
1277
+ }
1265
1278
  sumCallUsageInto(unpricedByModel, s.unpricedByModel);
1266
1279
  sumCallUsageInto(estimatedByModel, s.estimatedByModel);
1267
- const hasNoAttributableActivity = !hasAttributableActivity(s);
1280
+ totalCostUsd += todayCost.costUsd;
1281
+ // Subagent-only portion of the same session. This is what makes the KPI
1282
+ // cross-session: each `--stdio` process persists its own subagent cost, so
1283
+ // summing it here (rather than reading only THIS process's live
1284
+ // CostTracker) picks up subagent activity from any concurrent session.
1285
+ subagentUsd += todayCost.subagentCostUsd;
1268
1286
  const ratio = todayPortionRatio(s, now);
1269
- totalCostUsd +=
1270
- s.costByDayUsd !== undefined
1271
- ? (s.costByDayUsd[todayKey] ?? 0)
1272
- : hasNoAttributableActivity
1273
- ? 0
1274
- : todayPortionOfSessionCost(s, now);
1275
- // (2b) subagent-only portion of the same session. Day-bucket-first, else
1276
- // pro-rate — applying the same phantom guard explicitly rather than
1277
- // relying on hasNoAttributableActivity's subagentCostUsd===0 term to
1278
- // zero this out implicitly (that held today, but shouldn't be a silent
1279
- // invariant across two separate expressions). This is what makes the
1280
- // KPI cross-session: each `--stdio` process persists its own subagent
1281
- // cost, so summing it here (rather than reading only THIS process's
1282
- // live CostTracker) picks up subagent activity from any concurrent
1283
- // session.
1284
- subagentUsd +=
1285
- s.subagentCostByDayUsd !== undefined
1286
- ? (s.subagentCostByDayUsd[todayKey] ?? 0)
1287
- : hasNoAttributableActivity
1288
- ? 0
1289
- : (s.subagentCostUsd ?? 0) * ratio;
1290
1287
  // Cache token/dollar sums below carry the same unguarded overlap
1291
1288
  // characteristic subagentUsd/totalCostUsd already have on this line:
1292
1289
  // a session that's concurrently live in ANOTHER process (so
@@ -1314,6 +1311,7 @@ export function createApiHandler(deps) {
1314
1311
  }
1315
1312
  const sessionStatus = computeSessionStatusAggregate({
1316
1313
  replayCache: gitReplayCache,
1314
+ identityResolver: gitIdentityResolver,
1317
1315
  sessionIds: sessionsSeen,
1318
1316
  liveSet,
1319
1317
  startMs,
@@ -1323,8 +1321,16 @@ export function createApiHandler(deps) {
1323
1321
  });
1324
1322
  // (3) include this MCP's live session today-portion. Per-day attribution
1325
1323
  // comes from CostTracker, which buckets each token event by local-day at
1326
- // record time (see CostTracker.accumulateTokens). Falls back to session
1327
- // total if no per-day data is available (older deployments / first event).
1324
+ // record time (see CostTracker.accumulateTokens).
1325
+ //
1326
+ // The persisted loop above holds the live session back, so this top-up is
1327
+ // the only place its today cost is counted. It takes the larger of the
1328
+ // live tracker's figure and the session's own checkpoint figure, per key.
1329
+ // A seeded tracker already contains the checkpoint (equal, counted once),
1330
+ // and an unseeded one falls back to the checkpoint instead of losing it.
1331
+ // liveAlreadyPersisted separately gates the anti-pattern and efficiency
1332
+ // figures below, which the todaySessions walk counts for a checkpointed
1333
+ // session.
1328
1334
  const liveAlreadyPersisted = todaySessions.some((s) => s.sessionId === liveSid);
1329
1335
  // An unscoped aggregator (--local/proxy session id) runs its
1330
1336
  // SubagentWatcher unscoped — its own live CostTracker may already hold
@@ -1335,7 +1341,7 @@ export function createApiHandler(deps) {
1335
1341
  // `--stdio` session (including one still on a pending-* provisional id),
1336
1342
  // whose live cost really is exclusively its own.
1337
1343
  const liveIsUnscopedAggregator = isUnscopedAggregatorSessionId(liveSid);
1338
- if (!liveAlreadyPersisted && !liveIsUnscopedAggregator) {
1344
+ if (!liveIsUnscopedAggregator) {
1339
1345
  const liveMetrics = deps.costTracker?.getMetrics();
1340
1346
  sumCallUsageInto(unpricedByModel, liveMetrics?.unpricedByModel);
1341
1347
  sumCallUsageInto(estimatedByModel, liveMetrics?.estimatedByModel);
@@ -1343,8 +1349,9 @@ export function createApiHandler(deps) {
1343
1349
  // persisted-session loop) so the persisted-loop and this live top-up can
1344
1350
  // never split onto two independently-derived day keys.
1345
1351
  const liveTodayUsd = deps.costTracker?.getCostForDay?.(todayKey) ?? null;
1352
+ let liveTodayCostUsd = 0;
1346
1353
  if (typeof liveTodayUsd === 'number') {
1347
- totalCostUsd += liveTodayUsd;
1354
+ liveTodayCostUsd = liveTodayUsd;
1348
1355
  }
1349
1356
  else {
1350
1357
  // Fallback for older deployments without the per-day API. NEVER add the
@@ -1357,9 +1364,10 @@ export function createApiHandler(deps) {
1357
1364
  const startedTs = deps.sessionTracker?.getMetrics().sessionStartTime ?? null;
1358
1365
  const startedToday = typeof startedTs === 'number' && localDateKey(startedTs) === todayKey;
1359
1366
  if (typeof sessionCost === 'number' && startedToday) {
1360
- totalCostUsd += sessionCost;
1367
+ liveTodayCostUsd = sessionCost;
1361
1368
  }
1362
1369
  }
1370
+ totalCostUsd += Math.max(liveTodayCostUsd, liveCheckpointTodayUsd);
1363
1371
  // This process's own first-activity-today, covering the gap between
1364
1372
  // "drained from the buffer" and "persisted to a session file" — the
1365
1373
  // same gap liveTodayUsd above already covers for cost.
@@ -1369,44 +1377,45 @@ export function createApiHandler(deps) {
1369
1377
  dailyFirstActivityMs = liveFirstActivityMs;
1370
1378
  }
1371
1379
  }
1372
- // This MCP's own live subagent-for-day figure — the (2b) loop above
1373
- // already summed every OTHER today session's persisted subagentCostUsd;
1374
- // this process's own session isn't in that persisted set yet, so its
1375
- // contribution comes from the live, per-day-bucketed CostTracker value
1376
- // instead (same reasoning as liveTodayUsd above).
1377
- subagentUsd += deps.costTracker?.getSubagentCostForDay?.(todayKey) ?? 0;
1378
- // Cache top-up (dollars AND token counts) for this process's own live,
1379
- // not-yet-persisted session. These are session-cumulative totals with
1380
- // no per-day equivalent (unlike getCostForDay above), so — mirroring
1381
- // the startedToday guard the cost fallback above needs for the exact
1382
- // same reason — only add them when this live session actually started
1383
- // today; a cross-midnight live session's cumulative totals would
1384
- // otherwise leak yesterday's activity in too. Token counts must be
1385
- // topped up alongside the dollar figure: step (1)'s buffer union only
1386
- // sees the last undrained tail since the previous periodic checkpoint,
1387
- // so without this, totalSavingsUsd could reflect a session's full
1388
- // accumulated savings while totalCacheReadTokens/cacheHitRatePct/status
1389
- // reflected almost none of the activity that produced them.
1380
+ // This MCP's own live subagent-for-day figure. The (2b) loop above
1381
+ // summed every OTHER today session's persisted subagent cost and held
1382
+ // this session's back, so it joins here as the larger of the live
1383
+ // per-day CostTracker value and its own checkpoint (same reasoning as
1384
+ // the cost above).
1385
+ subagentUsd += Math.max(deps.costTracker?.getSubagentCostForDay?.(todayKey) ?? 0, liveCheckpointSubagentUsd);
1386
+ // Cache top-up (dollars AND token counts) for this process's own live
1387
+ // session, whether or not it has a checkpoint. These are
1388
+ // session-cumulative totals with no per-day equivalent (unlike
1389
+ // getCostForDay above), so, mirroring the startedToday guard the cost
1390
+ // fallback above needs for the exact same reason, only add them when
1391
+ // this live session actually started today; a cross-midnight live
1392
+ // session's cumulative totals would otherwise leak yesterday's activity
1393
+ // in too. They do not overlap step (1)'s buffer sum: the tracker holds
1394
+ // only events already drained from the buffer (plus the seeded
1395
+ // checkpoint), while peekAllBuffers() sees only the undrained tail. The
1396
+ // persisted loop skips this session's cache sums, so each token is
1397
+ // counted once. Token counts are topped up alongside the dollar figure
1398
+ // so totalSavingsUsd, totalCacheReadTokens, cacheHitRatePct and status
1399
+ // all describe the same activity.
1390
1400
  const liveStartedTs = deps.sessionTracker?.getMetrics().sessionStartTime ?? null;
1391
1401
  if (typeof liveStartedTs === 'number' && localDateKey(liveStartedTs) === todayKey) {
1392
1402
  const liveCostMetrics = deps.costTracker?.getMetrics();
1393
- cacheSavingsUsdSum += liveCostMetrics?.totalCacheSavingsUsd ?? 0;
1394
- cacheReadTokensSum += liveCostMetrics?.totalCacheReadTokens ?? 0;
1395
- cacheCreationTokensSum += liveCostMetrics?.totalCacheCreationTokens ?? 0;
1396
- cacheInputTokensSum += liveCostMetrics?.totalInputTokens ?? 0;
1403
+ cacheSavingsUsdSum += Math.max(liveCostMetrics?.totalCacheSavingsUsd ?? 0, liveCheckpointCacheSavingsUsd);
1404
+ cacheReadTokensSum += Math.max(liveCostMetrics?.totalCacheReadTokens ?? 0, liveCheckpointCacheReadTokens);
1405
+ cacheCreationTokensSum += Math.max(liveCostMetrics?.totalCacheCreationTokens ?? 0, liveCheckpointCacheCreationTokens);
1406
+ cacheInputTokensSum += Math.max(liveCostMetrics?.totalInputTokens ?? 0, liveCheckpointInputTokens);
1397
1407
  }
1398
1408
  }
1399
- // Live session anti-patterns (in-memory, not yet persisted).
1400
- // Mirror the alreadyPersisted guard used for cost above: if this MCP's
1401
- // session is already in todaySessions, its anti-patterns were counted in
1402
- // the loop above — don't double-count.
1409
+ // Live session anti-patterns (in-memory, not yet persisted). If this MCP's
1410
+ // session is already in todaySessions, its checkpointed anti-patterns were
1411
+ // counted in the todaySessions walk above, so don't double-count.
1403
1412
  if (deps.antiPatternDetector && !liveAlreadyPersisted) {
1404
1413
  const live = deps.antiPatternDetector.getCurrentPatterns();
1405
1414
  antiPatternCount += live.length;
1406
1415
  }
1407
1416
  // Efficiency KPI: average `efficiencyScore` across today's persisted sessions, plus
1408
1417
  // this process's own live score when its own session isn't in that persisted set yet
1409
- // (same liveAlreadyPersisted guard already used for cost/anti-patterns above).
1418
+ // (same liveAlreadyPersisted guard as the anti-patterns above).
1410
1419
  let efficiencyScoreSum = 0;
1411
1420
  let efficiencyScoreCount = 0;
1412
1421
  for (const s of todaySessions) {
@@ -2057,12 +2066,11 @@ export function createApiHandler(deps) {
2057
2066
  let historical = [];
2058
2067
  const historicalIdentities = new Map();
2059
2068
  if (deps.sessionStore?.loadAllSessions) {
2060
- const identityResolver = new WorktreeIdentityResolver();
2061
2069
  const sessions = deps.sessionStore.loadAllSessions({
2062
2070
  since: new Date(since),
2063
2071
  });
2064
2072
  for (const session of sessions) {
2065
- const replayed = gitReplayCache.replay(session, identityResolver);
2073
+ const replayed = gitReplayCache.replay(session, gitIdentityResolver);
2066
2074
  historical = historical.concat(replayed.records);
2067
2075
  for (const [key, identity] of replayed.identities) {
2068
2076
  historicalIdentities.set(key, identity);
@@ -2134,16 +2142,18 @@ export function createApiHandler(deps) {
2134
2142
  // stays comparable to `peak` below without re-filtering here.
2135
2143
  const allSessions = deps.sessionStore?.loadAllSessions?.() ?? [];
2136
2144
  const allTimePeak = computeTodayPeakConcurrency(allSessions);
2137
- // Prefer loadSessionsOverlappingToday() so a session that started
2138
- // yesterday and is still running isn't invisible to the chart —
2139
- // loadTodaySessions() filters by filename date (= start date) and
2140
- // would otherwise silently drop it. Downstream window-building already
2141
- // clamps each session's activity to [startTimestamp, dayEnd], so widening
2142
- // the input set here can't leak yesterday's activity into today's buckets.
2145
+ // Prefer loadSessionsWithActivityToday() so a session that started
2146
+ // earlier and is still running, or was resumed from an older start date
2147
+ // (keeping its original filename), isn't invisible to the chart —
2148
+ // loadTodaySessions() filters by filename date (= start date) and would
2149
+ // silently drop it. Downstream window-building clamps each session's
2150
+ // activity to [startTimestamp, dayEnd], so widening the input set here
2151
+ // can't leak earlier days' activity into today's buckets.
2143
2152
  // loadAllSessions()/loadTodaySessions() already exclude synthetic session
2144
2153
  // ids, so the chart buckets below agree with the session list shown
2145
2154
  // alongside the chart without re-filtering here.
2146
- const todaySessions = deps.sessionStore?.loadSessionsOverlappingToday?.() ??
2155
+ const todaySessions = deps.sessionStore?.loadSessionsWithActivityToday?.() ??
2156
+ deps.sessionStore?.loadSessionsOverlappingToday?.() ??
2147
2157
  deps.sessionStore?.loadTodaySessions() ??
2148
2158
  [];
2149
2159
  // 96 × 15-minute fixed-grid buckets covering today (local midnight →
@@ -2366,6 +2376,11 @@ export function createApiHandler(deps) {
2366
2376
  /* config file may not exist yet — fall through to startup defaults */
2367
2377
  }
2368
2378
  }
2379
+ // Sends read the file at call time and skip its URL when a command is
2380
+ // configured there, so follow the disk rather than the startup snapshot.
2381
+ const overrideSource = digestWebhookUrlOverrideSource(c) ??
2382
+ (isDigestWebhookCommandConfigured(disk) ? 'command' : undefined);
2383
+ const fileWebhookUrl = typeof disk.digestWebhookUrl === 'string' ? disk.digestWebhookUrl : null;
2369
2384
  const diskAlerts = (disk.alerts ?? {});
2370
2385
  const diskPersonal = (diskAlerts['personal'] ?? {});
2371
2386
  jsonOk(res, {
@@ -2378,7 +2393,10 @@ export function createApiHandler(deps) {
2378
2393
  retainSessionsDays: 'retainSessionsDays' in disk
2379
2394
  ? disk.retainSessionsDays
2380
2395
  : c.retainSessionsDays,
2381
- digestWebhookUrl: 'digestWebhookUrl' in disk ? disk.digestWebhookUrl : c.digestWebhookUrl,
2396
+ digestWebhookUrl: fileWebhookUrl,
2397
+ // Where sends actually read the URL from. The value above is only the
2398
+ // file's; env or a command outranks it, and its value is never returned.
2399
+ digestWebhookUrlSource: overrideSource ?? (fileWebhookUrl ? 'file' : 'unset'),
2382
2400
  digestSchedule: typeof disk.digestSchedule === 'string' ? disk.digestSchedule : c.digestSchedule,
2383
2401
  alerts: {
2384
2402
  personal: {