sidekick-shared 0.22.0 → 0.23.1

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 (120) hide show
  1. package/README.md +75 -63
  2. package/dist/accountManager.js +10 -0
  3. package/dist/accountRegistry.d.ts +2 -0
  4. package/dist/accountStatus.d.ts +1 -0
  5. package/dist/accountStatus.js +6 -0
  6. package/dist/accounts.js +2 -0
  7. package/dist/aggregation/EventAggregator.d.ts +36 -2
  8. package/dist/aggregation/EventAggregator.js +259 -33
  9. package/dist/aggregation/snapshot.d.ts +1 -1
  10. package/dist/aggregation/snapshot.js +2 -1
  11. package/dist/aggregation/types.d.ts +25 -1
  12. package/dist/analytics/codeImpact.d.ts +20 -0
  13. package/dist/analytics/codeImpact.js +21 -0
  14. package/dist/analytics/compactionLedger.d.ts +11 -0
  15. package/dist/analytics/compactionLedger.js +31 -0
  16. package/dist/analytics/qualityScore.d.ts +26 -0
  17. package/dist/analytics/qualityScore.js +115 -0
  18. package/dist/autoSwitch.d.ts +1 -1
  19. package/dist/autoSwitch.js +14 -3
  20. package/dist/codexProfiles.js +2 -0
  21. package/dist/codexQuota.d.ts +2 -0
  22. package/dist/codexQuota.js +27 -0
  23. package/dist/codexQuotaWatcher.d.ts +7 -0
  24. package/dist/codexQuotaWatcher.js +37 -19
  25. package/dist/context/composer.d.ts +3 -2
  26. package/dist/context/composer.js +5 -5
  27. package/dist/context/sessionContext.js +35 -9
  28. package/dist/credentialIO.js +19 -5
  29. package/dist/doctor.d.ts +55 -0
  30. package/dist/doctor.js +244 -0
  31. package/dist/errorHistory.d.ts +33 -0
  32. package/dist/errorHistory.js +95 -0
  33. package/dist/extractors/errorTaxonomy.d.ts +6 -0
  34. package/dist/extractors/errorTaxonomy.js +47 -0
  35. package/dist/extractors/toolCall.d.ts +9 -0
  36. package/dist/extractors/toolCall.js +79 -0
  37. package/dist/handoffUrl.d.ts +7 -0
  38. package/dist/handoffUrl.js +26 -0
  39. package/dist/index.d.ts +32 -9
  40. package/dist/index.js +64 -24
  41. package/dist/modelInfo.js +6 -0
  42. package/dist/multiProviderQuotaService.js +13 -2
  43. package/dist/parsers/codexParser.js +17 -1
  44. package/dist/parsers/openCodeParser.js +13 -0
  45. package/dist/parsers/planExtractor.js +17 -6
  46. package/dist/parsers/sessionActivityDetector.js +2 -0
  47. package/dist/parsers/sessionPathResolver.js +19 -16
  48. package/dist/parsers/subagentScanner.d.ts +1 -1
  49. package/dist/parsers/subagentScanner.js +4 -5
  50. package/dist/parsers/subagentTraceParser.js +4 -7
  51. package/dist/paths.d.ts +10 -0
  52. package/dist/paths.js +18 -5
  53. package/dist/projectMigration.d.ts +23 -0
  54. package/dist/projectMigration.js +95 -0
  55. package/dist/providerStatus.js +13 -2
  56. package/dist/providers/claudeCode.js +18 -4
  57. package/dist/providers/codex.js +42 -13
  58. package/dist/providers/codexDatabase.d.ts +1 -11
  59. package/dist/providers/detect.js +32 -3
  60. package/dist/providers/openCodeDatabase.d.ts +1 -31
  61. package/dist/providers/openCodeDatabase.js +21 -11
  62. package/dist/providers/types.d.ts +1 -0
  63. package/dist/quota.js +8 -0
  64. package/dist/quotaPoller.js +17 -6
  65. package/dist/quotaSnapshots.js +80 -26
  66. package/dist/readers/decisions.d.ts +2 -1
  67. package/dist/readers/decisions.js +5 -5
  68. package/dist/readers/handoff.d.ts +2 -1
  69. package/dist/readers/handoff.js +13 -8
  70. package/dist/readers/helpers.d.ts +3 -0
  71. package/dist/readers/helpers.js +12 -0
  72. package/dist/readers/notes.d.ts +2 -1
  73. package/dist/readers/notes.js +11 -8
  74. package/dist/readers/plans.js +3 -5
  75. package/dist/readers/tasks.d.ts +2 -1
  76. package/dist/readers/tasks.js +5 -5
  77. package/dist/report/htmlHelpers.js +6 -1
  78. package/dist/report/openBrowser.js +3 -3
  79. package/dist/schemas/index.d.ts +2 -0
  80. package/dist/schemas/index.js +10 -1
  81. package/dist/schemas/observedSessionV1.d.ts +499 -0
  82. package/dist/schemas/observedSessionV1.js +69 -0
  83. package/dist/schemas/sessionEvent.d.ts +13 -0
  84. package/dist/schemas/sessionEvent.js +19 -0
  85. package/dist/search/sessionSearch.d.ts +2 -2
  86. package/dist/search/sessionSearch.js +2 -3
  87. package/dist/sessionMonitor.d.ts +29 -0
  88. package/dist/sessionMonitor.js +71 -0
  89. package/dist/statusline/BurnRateCalculator.d.ts +14 -0
  90. package/dist/statusline/BurnRateCalculator.js +45 -0
  91. package/dist/statusline/formatter.d.ts +16 -0
  92. package/dist/statusline/formatter.js +84 -0
  93. package/dist/statusline/index.d.ts +7 -0
  94. package/dist/statusline/index.js +13 -0
  95. package/dist/terminalSync.js +16 -7
  96. package/dist/types/codex.d.ts +2 -0
  97. package/dist/types/historicalData.d.ts +49 -1
  98. package/dist/types/historicalData.js +28 -2
  99. package/dist/types/observedSessionV1.d.ts +70 -0
  100. package/dist/types/observedSessionV1.js +186 -0
  101. package/dist/types/opencode.d.ts +1 -0
  102. package/dist/types/sessionEvent.d.ts +19 -1
  103. package/dist/watchers/eventBridge.js +1 -0
  104. package/dist/watchers/factory.js +14 -3
  105. package/dist/watchers/jsonlTail.js +34 -14
  106. package/dist/watchers/jsonlWatcher.js +5 -3
  107. package/dist/watchers/providerReaderWatcher.js +4 -2
  108. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  109. package/dist/watchers/sqliteWatcher.js +27 -14
  110. package/dist/watchers/types.d.ts +5 -0
  111. package/dist/writers/atomic.d.ts +4 -0
  112. package/dist/writers/atomic.js +200 -0
  113. package/dist/writers/decisions.d.ts +12 -0
  114. package/dist/writers/decisions.js +88 -0
  115. package/dist/writers/notes.d.ts +8 -0
  116. package/dist/writers/notes.js +82 -0
  117. package/dist/writers/tasks.d.ts +6 -0
  118. package/dist/writers/tasks.js +100 -0
  119. package/dist/zaiQuotaWatcher.js +6 -0
  120. package/package.json +5 -1
@@ -9,7 +9,7 @@
9
9
  * @module aggregation/EventAggregator
10
10
  */
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.EventAggregator = void 0;
12
+ exports.EventAggregator = exports.SNAPSHOT_SCHEMA_VERSION = void 0;
13
13
  exports.parseTodoDependencies = parseTodoDependencies;
14
14
  const jsonl_1 = require("../parsers/jsonl");
15
15
  const planExtractor_1 = require("../parsers/planExtractor");
@@ -20,14 +20,16 @@ const FrequencyTracker_1 = require("./FrequencyTracker");
20
20
  const HeatmapTracker_1 = require("./HeatmapTracker");
21
21
  const PatternExtractor_1 = require("./PatternExtractor");
22
22
  const modelInfo_1 = require("../modelInfo");
23
+ const errorTaxonomy_1 = require("../extractors/errorTaxonomy");
23
24
  // ── Defaults ──
24
25
  const DEFAULT_TIMELINE_CAP = 200;
25
26
  const DEFAULT_LATENCY_CAP = 100;
27
+ const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
26
28
  const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
27
29
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
28
30
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
29
31
  /** Schema version for serialized snapshots. */
30
- const SNAPSHOT_SCHEMA_VERSION = 1;
32
+ exports.SNAPSHOT_SCHEMA_VERSION = 3;
31
33
  // ── Goal gate detection regex ──
32
34
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
33
35
  // ── Todo dependency parsing ──
@@ -79,6 +81,12 @@ class EventAggregator {
79
81
  // Context size tracking
80
82
  currentContextSize = 0;
81
83
  previousContextSize = 0;
84
+ /**
85
+ * Index of a heuristic compaction event still awaiting its real
86
+ * post-compaction size from the next usage event. Prevents the usage-drop
87
+ * detector from double-counting an explicit compaction marker.
88
+ */
89
+ pendingHeuristicCompactionIndex = null;
82
90
  // Compaction
83
91
  compactionEvents = [];
84
92
  // Truncation
@@ -86,6 +94,10 @@ class EventAggregator {
86
94
  // Tool analytics
87
95
  toolAnalytics = new Map();
88
96
  pendingToolCalls = new Map();
97
+ errorByToolCategory = new Map();
98
+ errorBuckets = new Map();
99
+ retryAttempts = 0;
100
+ finishReasons = new Map();
89
101
  // Burn rate
90
102
  burnSamples = [];
91
103
  lastBurnSampleTime = 0;
@@ -99,6 +111,8 @@ class EventAggregator {
99
111
  pendingSubagents = new Map(); // toolUseId -> index
100
112
  // Plan state
101
113
  planExtractor;
114
+ planState = null;
115
+ planEnteredAt;
102
116
  // Context attribution
103
117
  contextAttribution = {
104
118
  systemPrompt: 0,
@@ -114,6 +128,7 @@ class EventAggregator {
114
128
  permissionModeHistory = [];
115
129
  // Context timeline
116
130
  contextTimeline = [];
131
+ contextTimelineCap;
117
132
  contextTurnIndex = 0;
118
133
  // Timeline
119
134
  timeline = [];
@@ -137,6 +152,7 @@ class EventAggregator {
137
152
  this.latencyCap = options?.latencyCap ?? DEFAULT_LATENCY_CAP;
138
153
  this.burnWindowMs = options?.burnWindowMs ?? DEFAULT_BURN_WINDOW_MS;
139
154
  this.burnSampleMs = options?.burnSampleMs ?? DEFAULT_BURN_SAMPLE_MS;
155
+ this.contextTimelineCap = options?.contextTimelineCap ?? DEFAULT_CONTEXT_TIMELINE_CAP;
140
156
  this.computeContextSize = options?.computeContextSize ?? null;
141
157
  this.providerId = options?.providerId ?? null;
142
158
  this._providerId = this.providerId;
@@ -154,12 +170,17 @@ class EventAggregator {
154
170
  this.lastEventTime = event.timestamp;
155
171
  // Guard: some event types (e.g. 'summary') have no message field in the raw JSONL
156
172
  if (!event.message) {
173
+ if (event.type === 'summary') {
174
+ this.recordExplicitCompaction(event.timestamp, event.compaction);
175
+ this.addTimelineFromSessionEvent(event);
176
+ }
157
177
  return;
158
178
  }
159
179
  // 2. Track model
160
180
  if (event.message.model) {
161
181
  this.currentModel = event.message.model;
162
182
  }
183
+ this.recordProviderMetadata(event);
163
184
  // 3. Skip system/audit events and synthetic token-count events for messageCount
164
185
  const msgId = event.message.id ?? '';
165
186
  if (event.type !== 'system' && !msgId.startsWith('token-count-')) {
@@ -171,7 +192,8 @@ class EventAggregator {
171
192
  }
172
193
  // 5. Token accumulation
173
194
  if (event.message.usage) {
174
- this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
195
+ const usageCost = this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
196
+ this.attributePlanUsage(event.message.usage.input_tokens + event.message.usage.output_tokens, usageCost);
175
197
  }
176
198
  if (event.type !== 'system') {
177
199
  // 6. Tool extraction from content blocks
@@ -187,6 +209,9 @@ class EventAggregator {
187
209
  this.attributeContextFromEvent(event);
188
210
  // 11. Permission mode tracking
189
211
  this.trackPermissionMode(event);
212
+ if (event.type === 'summary') {
213
+ this.recordExplicitCompaction(event.timestamp, event.compaction);
214
+ }
190
215
  // 12. Timeline
191
216
  this.addTimelineFromSessionEvent(event);
192
217
  // 13. Gonzo/Lazyjournal analytics
@@ -222,6 +247,7 @@ class EventAggregator {
222
247
  this.outputTokens += outputTok;
223
248
  this.cacheReadTokens += cacheRead;
224
249
  this.cacheWriteTokens += cacheWrite;
250
+ this.attributePlanUsage(inputTok + outputTok, event.cost ?? 0);
225
251
  // Tokens since last burn sample
226
252
  this.tokensSinceLastSample += inputTok + outputTok;
227
253
  // Context size
@@ -238,13 +264,17 @@ class EventAggregator {
238
264
  contextSize = inputTok + cacheWrite + cacheRead;
239
265
  }
240
266
  // Compaction detection
241
- if (this.previousContextSize > 0 &&
267
+ if (this.resolvePendingHeuristicCompaction(contextSize)) {
268
+ // The drop belongs to the explicit compaction marker just recorded.
269
+ }
270
+ else if (this.previousContextSize > 0 &&
242
271
  contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
243
272
  this.compactionEvents.push({
244
273
  timestamp: new Date(event.timestamp),
245
274
  contextBefore: this.previousContextSize,
246
275
  contextAfter: contextSize,
247
276
  tokensReclaimed: this.previousContextSize - contextSize,
277
+ source: 'heuristic',
248
278
  });
249
279
  }
250
280
  this.previousContextSize = contextSize;
@@ -277,7 +307,7 @@ class EventAggregator {
277
307
  priced: true,
278
308
  };
279
309
  acc.calls++;
280
- acc.tokens += inputTok + outputTok;
310
+ acc.tokens += inputTok + outputTok + cacheWrite + cacheRead;
281
311
  acc.inputTokens += inputTok;
282
312
  acc.outputTokens += outputTok;
283
313
  acc.cacheWriteTokens += cacheWrite;
@@ -297,6 +327,7 @@ class EventAggregator {
297
327
  // 4. Tool tracking from FollowEvent
298
328
  if (event.type === 'tool_use' && event.toolName) {
299
329
  this.recordFollowToolUse(event);
330
+ this.attributePlanToolCall(event.toolName);
300
331
  }
301
332
  else if (event.type === 'tool_result') {
302
333
  this.recordFollowToolResult(event);
@@ -306,7 +337,7 @@ class EventAggregator {
306
337
  // 6. Subagent tracking
307
338
  this.extractSubagentFromFollowEvent(event);
308
339
  // 7. Plan extraction
309
- this.planExtractor.processEvent(event);
340
+ this.processPlanFollowEvent(event);
310
341
  // 8. Context attribution
311
342
  this.attributeContextFromFollowEvent(event);
312
343
  // 9. Truncation detection from tool results
@@ -315,19 +346,14 @@ class EventAggregator {
315
346
  }
316
347
  // 10. Explicit compaction event (summary type)
317
348
  if (event.type === 'summary') {
318
- this.compactionEvents.push({
319
- timestamp: new Date(event.timestamp),
320
- contextBefore: this.previousContextSize,
321
- contextAfter: 0,
322
- tokensReclaimed: this.previousContextSize,
323
- });
349
+ this.recordExplicitCompaction(event.timestamp, event.compaction);
324
350
  }
325
351
  // 11. Timeline
326
352
  this.addTimelineFromFollowEvent(event);
327
353
  // 12. Gonzo/Lazyjournal analytics
328
354
  this.feedAnalytics(event.timestamp, event.summary, event.toolName);
329
355
  // Message count (skip system events)
330
- if (event.type !== 'system') {
356
+ if (event.type === 'user' || event.type === 'assistant') {
331
357
  this.messageCount++;
332
358
  }
333
359
  }
@@ -354,6 +380,15 @@ class EventAggregator {
354
380
  b.pendingCount -
355
381
  (a.successCount + a.failureCount + a.pendingCount));
356
382
  }
383
+ getErrorRollup() {
384
+ return {
385
+ totalFailures: Array.from(this.errorByToolCategory.values()).reduce((sum, entry) => sum + entry.count, 0),
386
+ byToolCategory: Array.from(this.errorByToolCategory.values()).sort((a, b) => b.count - a.count),
387
+ byHourModel: Array.from(this.errorBuckets.values()).sort((a, b) => a.hour.localeCompare(b.hour)),
388
+ retryAttempts: this.retryAttempts,
389
+ finishReasons: Array.from(this.finishReasons, ([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count),
390
+ };
391
+ }
357
392
  getCompactionEvents() {
358
393
  return [...this.compactionEvents];
359
394
  }
@@ -379,10 +414,9 @@ class EventAggregator {
379
414
  return [...this.subagents];
380
415
  }
381
416
  getPlan() {
382
- const extracted = this.planExtractor.plan;
383
- if (!extracted)
384
- return null;
385
- return this.convertExtractedPlanToPlanState(extracted);
417
+ return this.planState
418
+ ? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
419
+ : null;
386
420
  }
387
421
  getContextAttribution() {
388
422
  return { ...this.contextAttribution };
@@ -424,6 +458,7 @@ class EventAggregator {
424
458
  truncationCount: this.truncationEvents.length,
425
459
  truncationEvents: this.getTruncationEvents(),
426
460
  toolStats: this.getToolStats(),
461
+ errorRollup: this.getErrorRollup(),
427
462
  burnRate: this.getBurnRate(),
428
463
  taskState: this.getTaskState(),
429
464
  subagents: this.getSubagents(),
@@ -455,6 +490,10 @@ class EventAggregator {
455
490
  this.truncationEvents = [];
456
491
  this.toolAnalytics.clear();
457
492
  this.pendingToolCalls.clear();
493
+ this.errorByToolCategory.clear();
494
+ this.errorBuckets.clear();
495
+ this.retryAttempts = 0;
496
+ this.finishReasons.clear();
458
497
  this.burnSamples = [];
459
498
  this.lastBurnSampleTime = 0;
460
499
  this.tokensSinceLastSample = 0;
@@ -464,6 +503,8 @@ class EventAggregator {
464
503
  this.subagents = [];
465
504
  this.pendingSubagents.clear();
466
505
  this.planExtractor.reset();
506
+ this.planState = null;
507
+ this.planEnteredAt = undefined;
467
508
  this.currentPermissionMode = null;
468
509
  this.permissionModeHistory = [];
469
510
  this.contextTimeline = [];
@@ -509,7 +550,7 @@ class EventAggregator {
509
550
  /** Serializes all mutable state to a JSON-safe object for snapshot persistence. */
510
551
  serialize() {
511
552
  return {
512
- version: SNAPSHOT_SCHEMA_VERSION,
553
+ version: exports.SNAPSHOT_SCHEMA_VERSION,
513
554
  tokens: {
514
555
  input: this.inputTokens,
515
556
  output: this.outputTokens,
@@ -523,6 +564,10 @@ class EventAggregator {
523
564
  compactionEvents: this.compactionEvents,
524
565
  truncationEvents: this.truncationEvents,
525
566
  toolAnalytics: Array.from(this.toolAnalytics.entries()),
567
+ errorByToolCategory: Array.from(this.errorByToolCategory.entries()),
568
+ errorBuckets: Array.from(this.errorBuckets.entries()),
569
+ retryAttempts: this.retryAttempts,
570
+ finishReasons: Array.from(this.finishReasons.entries()),
526
571
  contextAttribution: { ...this.contextAttribution },
527
572
  burnSamples: [...this.burnSamples],
528
573
  lastBurnSampleTime: this.lastBurnSampleTime,
@@ -549,8 +594,8 @@ class EventAggregator {
549
594
  }
550
595
  /** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
551
596
  restore(state) {
552
- if (state.version !== SNAPSHOT_SCHEMA_VERSION) {
553
- return; // Incompatible snapshot — caller should fall back to full replay
597
+ if (state.version !== exports.SNAPSHOT_SCHEMA_VERSION) {
598
+ return false;
554
599
  }
555
600
  this.inputTokens = state.tokens.input;
556
601
  this.outputTokens = state.tokens.output;
@@ -563,6 +608,10 @@ class EventAggregator {
563
608
  this.compactionEvents = [...state.compactionEvents];
564
609
  this.truncationEvents = [...state.truncationEvents];
565
610
  this.toolAnalytics = new Map(state.toolAnalytics);
611
+ this.errorByToolCategory = new Map(state.errorByToolCategory);
612
+ this.errorBuckets = new Map(state.errorBuckets);
613
+ this.retryAttempts = state.retryAttempts;
614
+ this.finishReasons = new Map(state.finishReasons);
566
615
  this.contextAttribution = { ...state.contextAttribution };
567
616
  this.burnSamples = [...state.burnSamples];
568
617
  this.lastBurnSampleTime = state.lastBurnSampleTime;
@@ -575,7 +624,9 @@ class EventAggregator {
575
624
  this.permissionModeHistory = state.permissionModeHistory
576
625
  ? [...state.permissionModeHistory]
577
626
  : [];
578
- this.contextTimeline = state.contextTimeline ? [...state.contextTimeline] : [];
627
+ this.contextTimeline = state.contextTimeline
628
+ ? state.contextTimeline.slice(-this.contextTimelineCap)
629
+ : [];
579
630
  this.contextTurnIndex = state.contextTurnIndex ?? 0;
580
631
  this.timeline = [...state.timeline];
581
632
  this.messageCount = state.messageCount;
@@ -598,6 +649,7 @@ class EventAggregator {
598
649
  this.pendingSubagents.clear();
599
650
  this.pendingUserRequest = null;
600
651
  this.planExtractor.reset();
652
+ return true;
601
653
  }
602
654
  // ═══════════════════════════════════════════════════════════════════════
603
655
  // Private: Token & Context
@@ -653,13 +705,17 @@ class EventAggregator {
653
705
  contextSize = inputTok + cacheWrite + cacheRead;
654
706
  }
655
707
  // Compaction detection
656
- if (this.previousContextSize > 0 &&
708
+ if (this.resolvePendingHeuristicCompaction(contextSize)) {
709
+ // The drop belongs to the explicit compaction marker just recorded.
710
+ }
711
+ else if (this.previousContextSize > 0 &&
657
712
  contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
658
713
  this.compactionEvents.push({
659
714
  timestamp: new Date(timestamp),
660
715
  contextBefore: this.previousContextSize,
661
716
  contextAfter: contextSize,
662
717
  tokensReclaimed: this.previousContextSize - contextSize,
718
+ source: 'heuristic',
663
719
  });
664
720
  }
665
721
  this.previousContextSize = contextSize;
@@ -670,6 +726,9 @@ class EventAggregator {
670
726
  inputTokens: contextSize,
671
727
  turnIndex: this.contextTurnIndex++,
672
728
  });
729
+ if (this.contextTimeline.length > this.contextTimelineCap) {
730
+ this.contextTimeline.splice(0, this.contextTimeline.length - this.contextTimelineCap);
731
+ }
673
732
  // Per-model usage. Accumulate reasoning tokens and an inverted-sticky
674
733
  // `priced` flag so one unpriced event taints the aggregate (UI renders "—").
675
734
  const perModelKey = model ?? this.currentModel ?? 'unknown';
@@ -685,7 +744,7 @@ class EventAggregator {
685
744
  priced: true,
686
745
  };
687
746
  acc.calls++;
688
- acc.tokens += inputTok + outputTok;
747
+ acc.tokens += inputTok + outputTok + cacheWrite + cacheRead;
689
748
  acc.inputTokens += inputTok;
690
749
  acc.outputTokens += outputTok;
691
750
  acc.cacheWriteTokens += cacheWrite;
@@ -697,6 +756,7 @@ class EventAggregator {
697
756
  this.modelUsage.set(perModelKey, acc);
698
757
  // Burn rate sampling
699
758
  this.updateBurnRate(timestamp);
759
+ return cost;
700
760
  }
701
761
  // ═══════════════════════════════════════════════════════════════════════
702
762
  // Private: Burn Rate
@@ -788,6 +848,7 @@ class EventAggregator {
788
848
  name,
789
849
  startTime: new Date(event.timestamp),
790
850
  });
851
+ this.attributePlanToolCall(name);
791
852
  }
792
853
  else if (block.type === 'tool_result') {
793
854
  // User content: tool_result block
@@ -797,6 +858,9 @@ class EventAggregator {
797
858
  continue;
798
859
  const pending = this.pendingToolCalls.get(toolUseId);
799
860
  if (pending) {
861
+ if (isError) {
862
+ this.recordError(pending.name, (0, errorTaxonomy_1.categorizeError)(block.content), event.timestamp, this.currentModel ?? 'unknown');
863
+ }
800
864
  this.pendingToolCalls.delete(toolUseId);
801
865
  const analytics = this.toolAnalytics.get(pending.name);
802
866
  if (analytics) {
@@ -890,6 +954,7 @@ class EventAggregator {
890
954
  const isError = raw.is_error === true;
891
955
  if (isError) {
892
956
  analytics.failureCount++;
957
+ this.recordError(pending.name, (0, errorTaxonomy_1.categorizeError)(raw.content ?? event.summary), event.timestamp, event.model ?? this.currentModel ?? 'unknown');
893
958
  }
894
959
  else {
895
960
  analytics.successCount++;
@@ -900,6 +965,36 @@ class EventAggregator {
900
965
  }
901
966
  }
902
967
  }
968
+ recordProviderMetadata(event) {
969
+ const metadata = event.providerMetadata;
970
+ if (!metadata)
971
+ return;
972
+ this.retryAttempts += metadata.retryAttempts?.length ?? 0;
973
+ for (const reason of metadata.finishReasons ?? []) {
974
+ this.finishReasons.set(reason, (this.finishReasons.get(reason) ?? 0) + 1);
975
+ }
976
+ if (metadata.providerError) {
977
+ this.recordError('ProviderAPI', (0, errorTaxonomy_1.categorizeError)(metadata.providerError, metadata.providerError.type), event.timestamp, event.message.model ?? this.currentModel ?? 'unknown');
978
+ }
979
+ }
980
+ recordError(tool, category, timestamp, model) {
981
+ const categoryKey = `${tool}\u0000${category}`;
982
+ const current = this.errorByToolCategory.get(categoryKey);
983
+ if (current)
984
+ current.count++;
985
+ else
986
+ this.errorByToolCategory.set(categoryKey, { tool, category, count: 1 });
987
+ const parsed = new Date(timestamp);
988
+ const hour = Number.isNaN(parsed.getTime())
989
+ ? timestamp.slice(0, 13)
990
+ : parsed.toISOString().slice(0, 13);
991
+ const bucketKey = `${hour}\u0000${model}\u0000${tool}\u0000${category}`;
992
+ const bucket = this.errorBuckets.get(bucketKey);
993
+ if (bucket)
994
+ bucket.count++;
995
+ else
996
+ this.errorBuckets.set(bucketKey, { hour, model, tool, category, count: 1 });
997
+ }
903
998
  detectTruncationFromFollowEvent(event) {
904
999
  const raw = event.raw;
905
1000
  if (!raw)
@@ -936,6 +1031,49 @@ class EventAggregator {
936
1031
  }
937
1032
  }
938
1033
  }
1034
+ recordExplicitCompaction(timestamp, reported) {
1035
+ const hasReported = reported !== undefined &&
1036
+ Number.isFinite(reported.tokensBefore) &&
1037
+ Number.isFinite(reported.tokensAfter) &&
1038
+ reported.tokensBefore >= reported.tokensAfter &&
1039
+ reported.tokensAfter >= 0;
1040
+ // Without reported counts, assume the full context was reclaimed until
1041
+ // the next usage event supplies the real post-compaction size.
1042
+ const contextBefore = hasReported ? reported.tokensBefore : this.previousContextSize;
1043
+ const contextAfter = hasReported ? reported.tokensAfter : 0;
1044
+ this.compactionEvents.push({
1045
+ timestamp: new Date(timestamp),
1046
+ contextBefore,
1047
+ contextAfter,
1048
+ tokensReclaimed: Math.max(0, contextBefore - contextAfter),
1049
+ source: hasReported ? 'reported' : 'heuristic',
1050
+ });
1051
+ if (hasReported) {
1052
+ this.previousContextSize = contextAfter;
1053
+ this.currentContextSize = contextAfter;
1054
+ }
1055
+ else {
1056
+ this.pendingHeuristicCompactionIndex = this.compactionEvents.length - 1;
1057
+ }
1058
+ }
1059
+ /**
1060
+ * Fills a pending heuristic compaction with the first observed
1061
+ * post-compaction context size. Returns true when the observed drop was
1062
+ * consumed by that pending event, so the caller must not record it again.
1063
+ */
1064
+ resolvePendingHeuristicCompaction(contextSize) {
1065
+ if (this.pendingHeuristicCompactionIndex === null)
1066
+ return false;
1067
+ const compaction = this.compactionEvents[this.pendingHeuristicCompactionIndex];
1068
+ this.pendingHeuristicCompactionIndex = null;
1069
+ if (!compaction)
1070
+ return false;
1071
+ if (contextSize < compaction.contextBefore) {
1072
+ compaction.contextAfter = contextSize;
1073
+ compaction.tokensReclaimed = compaction.contextBefore - contextSize;
1074
+ }
1075
+ return true;
1076
+ }
939
1077
  // ═══════════════════════════════════════════════════════════════════════
940
1078
  // Private: Task State from SessionEvent
941
1079
  // ═══════════════════════════════════════════════════════════════════════
@@ -962,6 +1100,7 @@ class EventAggregator {
962
1100
  }
963
1101
  else if (name === 'TaskUpdate') {
964
1102
  this.applyTaskUpdate(input);
1103
+ this.syncPlanStepFromTaskUpdate(input, new Date(event.timestamp));
965
1104
  }
966
1105
  else if (name === 'TodoWrite') {
967
1106
  this.handleTodoWrite(input, new Date(event.timestamp));
@@ -1005,6 +1144,7 @@ class EventAggregator {
1005
1144
  }
1006
1145
  else if (event.toolName === 'TaskUpdate') {
1007
1146
  this.applyTaskUpdate(input);
1147
+ this.syncPlanStepFromTaskUpdate(input, new Date(event.timestamp));
1008
1148
  }
1009
1149
  else if (event.toolName === 'TodoWrite') {
1010
1150
  this.handleTodoWrite(input, new Date(event.timestamp));
@@ -1102,6 +1242,36 @@ class EventAggregator {
1102
1242
  }
1103
1243
  }
1104
1244
  }
1245
+ syncPlanStepFromTaskUpdate(input, now) {
1246
+ if (!this.planState || !input.status)
1247
+ return;
1248
+ const taskId = String(input.taskId ?? input.task_id ?? '');
1249
+ const stepId = taskId.startsWith('plan-') ? `step-${taskId.slice(5)}` : taskId;
1250
+ const step = this.planState.steps.find((candidate) => candidate.id === stepId);
1251
+ if (!step)
1252
+ return;
1253
+ const rawStatus = String(input.status).toLowerCase();
1254
+ const status = rawStatus === 'completed'
1255
+ ? 'completed'
1256
+ : rawStatus === 'in_progress' || rawStatus === 'in-progress'
1257
+ ? 'in_progress'
1258
+ : rawStatus === 'deleted'
1259
+ ? 'skipped'
1260
+ : 'pending';
1261
+ if (status === 'in_progress' && step.status !== 'in_progress') {
1262
+ step.startedAt = now.toISOString();
1263
+ }
1264
+ if (status === 'completed' && step.status !== status) {
1265
+ step.startedAt ??= now.toISOString();
1266
+ step.completedAt = now.toISOString();
1267
+ step.durationMs = Math.max(0, now.getTime() - new Date(step.startedAt).getTime());
1268
+ }
1269
+ step.status = status;
1270
+ const completed = this.planState.steps.filter((candidate) => candidate.status === 'completed');
1271
+ this.planState.completionRate =
1272
+ this.planState.steps.length > 0 ? completed.length / this.planState.steps.length : 0;
1273
+ this.planState.active = this.planState.steps.some((candidate) => candidate.status === 'pending' || candidate.status === 'in_progress');
1274
+ }
1105
1275
  resolveTaskCreate(toolUseId, content, timestamp, isError) {
1106
1276
  const pending = this.pendingTaskCreates.get(toolUseId);
1107
1277
  if (!pending)
@@ -1408,27 +1578,83 @@ class EventAggregator {
1408
1578
  const providerId = this._providerId ?? 'claude-code';
1409
1579
  const followEvents = (0, eventBridge_1.toFollowEvents)(event, providerId);
1410
1580
  for (const fe of followEvents) {
1411
- this.planExtractor.processEvent(fe);
1581
+ this.processPlanFollowEvent(fe);
1412
1582
  }
1413
1583
  }
1414
- convertExtractedPlanToPlanState(extracted) {
1584
+ processPlanFollowEvent(event) {
1585
+ const now = new Date(event.timestamp);
1586
+ if (event.type === 'tool_use' && event.toolName === 'EnterPlanMode') {
1587
+ this.planEnteredAt = now;
1588
+ this.planState = {
1589
+ active: true,
1590
+ steps: [],
1591
+ enteredAt: now,
1592
+ source: event.providerId,
1593
+ };
1594
+ }
1595
+ const updated = this.planExtractor.processEvent(event);
1596
+ if (updated && this.planExtractor.plan) {
1597
+ this.planState = this.convertExtractedPlanToPlanState(this.planExtractor.plan, now);
1598
+ }
1599
+ if (event.type === 'tool_use' && event.toolName === 'ExitPlanMode' && this.planState) {
1600
+ this.planState.exitedAt = now;
1601
+ if (this.planState.enteredAt) {
1602
+ this.planState.totalDurationMs = now.getTime() - this.planState.enteredAt.getTime();
1603
+ }
1604
+ }
1605
+ }
1606
+ convertExtractedPlanToPlanState(extracted, now) {
1607
+ const previous = this.planState;
1608
+ const previousSteps = new Map((previous?.steps ?? []).map((step) => [`${step.id}\u0000${step.description}`, step]));
1415
1609
  const completed = extracted.steps.filter((s) => s.status === 'completed').length;
1416
1610
  const total = extracted.steps.length;
1611
+ const steps = extracted.steps.map((step) => {
1612
+ const old = previousSteps.get(`${step.id}\u0000${step.description}`);
1613
+ const merged = { ...step, ...old, status: step.status };
1614
+ if (step.status === 'in_progress' && old?.status !== 'in_progress' && !merged.startedAt) {
1615
+ merged.startedAt = now.toISOString();
1616
+ }
1617
+ if ((step.status === 'completed' || step.status === 'failed') &&
1618
+ old?.status !== step.status &&
1619
+ !merged.completedAt) {
1620
+ merged.startedAt ??= previous?.enteredAt?.toISOString() ?? now.toISOString();
1621
+ merged.completedAt = now.toISOString();
1622
+ merged.durationMs = Math.max(0, now.getTime() - new Date(merged.startedAt).getTime());
1623
+ }
1624
+ return merged;
1625
+ });
1417
1626
  return {
1418
- active: true,
1419
- steps: extracted.steps.map((s) => ({
1420
- id: s.id,
1421
- description: s.description,
1422
- status: s.status,
1423
- phase: s.phase,
1424
- complexity: s.complexity,
1425
- })),
1627
+ active: steps.some((step) => step.status === 'pending' || step.status === 'in_progress'),
1628
+ steps,
1426
1629
  title: extracted.title,
1427
1630
  source: extracted.source,
1428
1631
  completionRate: total > 0 ? completed / total : 0,
1429
1632
  rawMarkdown: extracted.rawMarkdown,
1633
+ enteredAt: previous?.enteredAt ?? this.planEnteredAt ?? now,
1634
+ exitedAt: previous?.exitedAt,
1635
+ totalDurationMs: previous?.totalDurationMs,
1636
+ prompt: previous?.prompt,
1637
+ revision: previous?.revision,
1430
1638
  };
1431
1639
  }
1640
+ getActivePlanStep() {
1641
+ return this.planState?.steps.find((step) => step.status === 'in_progress');
1642
+ }
1643
+ attributePlanUsage(tokens, costUsd) {
1644
+ const step = this.getActivePlanStep();
1645
+ if (!step)
1646
+ return;
1647
+ step.tokensUsed = (step.tokensUsed ?? 0) + tokens;
1648
+ if (costUsd > 0)
1649
+ step.costUsd = (step.costUsd ?? 0) + costUsd;
1650
+ }
1651
+ attributePlanToolCall(toolName) {
1652
+ if (['EnterPlanMode', 'ExitPlanMode', 'UpdatePlan', 'TaskUpdate'].includes(toolName))
1653
+ return;
1654
+ const step = this.getActivePlanStep();
1655
+ if (step)
1656
+ step.toolCalls = (step.toolCalls ?? 0) + 1;
1657
+ }
1432
1658
  // ═══════════════════════════════════════════════════════════════════════
1433
1659
  // Private: Context Attribution from SessionEvent
1434
1660
  // ═══════════════════════════════════════════════════════════════════════
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module aggregation/snapshot
9
9
  */
10
- import type { SerializedAggregatorState } from './EventAggregator';
10
+ import { type SerializedAggregatorState } from './EventAggregator';
11
11
  /** Snapshot sidecar file content. */
12
12
  export interface SessionSnapshot {
13
13
  /** Schema version for forward-compatibility checks. */
@@ -50,8 +50,9 @@ exports.isSnapshotValid = isSnapshotValid;
50
50
  const fs = __importStar(require("fs"));
51
51
  const path = __importStar(require("path"));
52
52
  const paths_1 = require("../paths");
53
+ const EventAggregator_1 = require("./EventAggregator");
53
54
  /** Current schema version — bump when format changes. */
54
- const SNAPSHOT_VERSION = 1;
55
+ const SNAPSHOT_VERSION = EventAggregator_1.SNAPSHOT_SCHEMA_VERSION;
55
56
  /** Returns the directory for snapshot files. */
56
57
  function getSnapshotsDir() {
57
58
  return path.join((0, paths_1.getConfigDir)(), 'snapshots');
@@ -2,14 +2,37 @@
2
2
  * Aggregated metrics output types for EventAggregator.
3
3
  * Re-exports canonical types from sessionEvent and defines aggregation-specific shapes.
4
4
  */
5
- export type { SessionEvent, ClaudeSessionEvent, MessageUsage, TokenUsage, ToolCall, ToolAnalytics, TimelineEvent, PendingToolCall, TrackedTask, TaskState, TaskStatus, SubagentStats, PlanState, PlanStep, CompactionEvent, TruncationEvent, ContextAttribution, TurnAttribution, ContextSizePoint, PendingUserRequest, ResponseLatency, LatencyStats, SessionStats, PermissionMode, PermissionModeChange, } from '../types/sessionEvent';
5
+ export type { SessionEvent, MessageUsage, TokenUsage, ToolCall, ToolAnalytics, TimelineEvent, PendingToolCall, TrackedTask, TaskState, TaskStatus, SubagentStats, PlanState, PlanStep, CompactionEvent, TruncationEvent, ContextAttribution, TurnAttribution, ContextSizePoint, PendingUserRequest, ResponseLatency, LatencyStats, SessionStats, PermissionMode, PermissionModeChange, } from '../types/sessionEvent';
6
6
  import type { ContextAttribution, ContextSizePoint, CompactionEvent, TruncationEvent, ToolAnalytics, TimelineEvent, TaskState, PlanState, LatencyStats, PermissionMode, PermissionModeChange } from '../types/sessionEvent';
7
+ import type { ErrorCategory } from '../extractors/errorTaxonomy';
8
+ export interface ErrorRollupEntry {
9
+ tool: string;
10
+ category: ErrorCategory;
11
+ count: number;
12
+ }
13
+ export interface ErrorBucketMetric extends ErrorRollupEntry {
14
+ /** ISO hour bucket, e.g. 2026-07-18T14. */
15
+ hour: string;
16
+ model: string;
17
+ }
18
+ export interface ErrorRollup {
19
+ totalFailures: number;
20
+ byToolCategory: ErrorRollupEntry[];
21
+ byHourModel: ErrorBucketMetric[];
22
+ retryAttempts: number;
23
+ finishReasons: Array<{
24
+ reason: string;
25
+ count: number;
26
+ }>;
27
+ }
7
28
  /** Configuration options for EventAggregator. */
8
29
  export interface EventAggregatorOptions {
9
30
  /** Maximum timeline events to retain (default 200). */
10
31
  timelineCap?: number;
11
32
  /** Maximum latency records to retain (default 100). */
12
33
  latencyCap?: number;
34
+ /** Maximum context-size points to retain (default 2000). */
35
+ contextTimelineCap?: number;
13
36
  /** Burn rate sliding window in ms (default 5 * 60_000). */
14
37
  burnWindowMs?: number;
15
38
  /** Burn rate sample interval in ms (default 10_000). */
@@ -105,6 +128,7 @@ export interface AggregatedMetrics {
105
128
  truncationCount: number;
106
129
  truncationEvents: TruncationEvent[];
107
130
  toolStats: ToolAnalytics[];
131
+ errorRollup: ErrorRollup;
108
132
  burnRate: BurnRateInfo;
109
133
  taskState: TaskState;
110
134
  subagents: SubagentLifecycle[];