sidekick-shared 0.21.6 → 0.23.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 (86) hide show
  1. package/README.md +51 -43
  2. package/dist/accountRegistry.d.ts +2 -0
  3. package/dist/accountStatus.d.ts +1 -0
  4. package/dist/accountStatus.js +6 -0
  5. package/dist/accounts.js +2 -0
  6. package/dist/aggregation/EventAggregator.d.ts +32 -1
  7. package/dist/aggregation/EventAggregator.js +238 -25
  8. package/dist/aggregation/snapshot.js +1 -1
  9. package/dist/aggregation/types.d.ts +23 -1
  10. package/dist/analytics/codeImpact.d.ts +20 -0
  11. package/dist/analytics/codeImpact.js +21 -0
  12. package/dist/analytics/compactionLedger.d.ts +11 -0
  13. package/dist/analytics/compactionLedger.js +31 -0
  14. package/dist/analytics/qualityScore.d.ts +26 -0
  15. package/dist/analytics/qualityScore.js +115 -0
  16. package/dist/codexProfiles.js +2 -0
  17. package/dist/context/composer.d.ts +3 -2
  18. package/dist/context/composer.js +5 -5
  19. package/dist/doctor.d.ts +55 -0
  20. package/dist/doctor.js +244 -0
  21. package/dist/errorHistory.d.ts +32 -0
  22. package/dist/errorHistory.js +94 -0
  23. package/dist/extractors/errorTaxonomy.d.ts +6 -0
  24. package/dist/extractors/errorTaxonomy.js +47 -0
  25. package/dist/extractors/toolCall.d.ts +9 -0
  26. package/dist/extractors/toolCall.js +79 -0
  27. package/dist/handoffUrl.d.ts +7 -0
  28. package/dist/handoffUrl.js +26 -0
  29. package/dist/index.d.ts +32 -9
  30. package/dist/index.js +62 -24
  31. package/dist/parsers/codexParser.js +13 -0
  32. package/dist/parsers/openCodeParser.js +13 -0
  33. package/dist/parsers/planExtractor.js +17 -6
  34. package/dist/paths.d.ts +10 -0
  35. package/dist/paths.js +18 -5
  36. package/dist/projectMigration.d.ts +23 -0
  37. package/dist/projectMigration.js +95 -0
  38. package/dist/providers/codexDatabase.d.ts +1 -11
  39. package/dist/providers/openCodeDatabase.d.ts +1 -31
  40. package/dist/providers/openCodeDatabase.js +9 -8
  41. package/dist/providers/types.d.ts +1 -0
  42. package/dist/readers/decisions.d.ts +2 -1
  43. package/dist/readers/decisions.js +2 -4
  44. package/dist/readers/handoff.d.ts +2 -1
  45. package/dist/readers/handoff.js +13 -8
  46. package/dist/readers/helpers.d.ts +3 -0
  47. package/dist/readers/helpers.js +12 -0
  48. package/dist/readers/notes.d.ts +2 -1
  49. package/dist/readers/notes.js +2 -4
  50. package/dist/readers/plans.js +1 -1
  51. package/dist/readers/tasks.d.ts +2 -1
  52. package/dist/readers/tasks.js +2 -4
  53. package/dist/schemas/index.d.ts +2 -0
  54. package/dist/schemas/index.js +10 -1
  55. package/dist/schemas/observedSessionV1.d.ts +499 -0
  56. package/dist/schemas/observedSessionV1.js +69 -0
  57. package/dist/schemas/sessionEvent.d.ts +13 -0
  58. package/dist/schemas/sessionEvent.js +19 -0
  59. package/dist/search/sessionSearch.d.ts +2 -2
  60. package/dist/search/sessionSearch.js +2 -3
  61. package/dist/sessionMonitor.d.ts +29 -0
  62. package/dist/sessionMonitor.js +71 -0
  63. package/dist/statusline/BurnRateCalculator.d.ts +14 -0
  64. package/dist/statusline/BurnRateCalculator.js +45 -0
  65. package/dist/statusline/formatter.d.ts +16 -0
  66. package/dist/statusline/formatter.js +77 -0
  67. package/dist/statusline/index.d.ts +7 -0
  68. package/dist/statusline/index.js +13 -0
  69. package/dist/types/codex.d.ts +2 -0
  70. package/dist/types/historicalData.d.ts +35 -1
  71. package/dist/types/historicalData.js +6 -2
  72. package/dist/types/observedSessionV1.d.ts +70 -0
  73. package/dist/types/observedSessionV1.js +185 -0
  74. package/dist/types/opencode.d.ts +1 -0
  75. package/dist/types/sessionEvent.d.ts +18 -1
  76. package/dist/watchers/eventBridge.js +1 -0
  77. package/dist/watchers/types.d.ts +5 -0
  78. package/dist/writers/atomic.d.ts +4 -0
  79. package/dist/writers/atomic.js +113 -0
  80. package/dist/writers/decisions.d.ts +12 -0
  81. package/dist/writers/decisions.js +88 -0
  82. package/dist/writers/notes.d.ts +8 -0
  83. package/dist/writers/notes.js +82 -0
  84. package/dist/writers/tasks.d.ts +6 -0
  85. package/dist/writers/tasks.js +100 -0
  86. package/package.json +5 -1
@@ -20,6 +20,7 @@ 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,7 +28,7 @@ const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
27
28
  const DEFAULT_BURN_SAMPLE_MS = 10_000;
28
29
  const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
29
30
  /** Schema version for serialized snapshots. */
30
- const SNAPSHOT_SCHEMA_VERSION = 1;
31
+ const SNAPSHOT_SCHEMA_VERSION = 3;
31
32
  // ── Goal gate detection regex ──
32
33
  const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
33
34
  // ── Todo dependency parsing ──
@@ -79,6 +80,12 @@ class EventAggregator {
79
80
  // Context size tracking
80
81
  currentContextSize = 0;
81
82
  previousContextSize = 0;
83
+ /**
84
+ * Index of a heuristic compaction event still awaiting its real
85
+ * post-compaction size from the next usage event. Prevents the usage-drop
86
+ * detector from double-counting an explicit compaction marker.
87
+ */
88
+ pendingHeuristicCompactionIndex = null;
82
89
  // Compaction
83
90
  compactionEvents = [];
84
91
  // Truncation
@@ -86,6 +93,10 @@ class EventAggregator {
86
93
  // Tool analytics
87
94
  toolAnalytics = new Map();
88
95
  pendingToolCalls = new Map();
96
+ errorByToolCategory = new Map();
97
+ errorBuckets = new Map();
98
+ retryAttempts = 0;
99
+ finishReasons = new Map();
89
100
  // Burn rate
90
101
  burnSamples = [];
91
102
  lastBurnSampleTime = 0;
@@ -99,6 +110,8 @@ class EventAggregator {
99
110
  pendingSubagents = new Map(); // toolUseId -> index
100
111
  // Plan state
101
112
  planExtractor;
113
+ planState = null;
114
+ planEnteredAt;
102
115
  // Context attribution
103
116
  contextAttribution = {
104
117
  systemPrompt: 0,
@@ -160,6 +173,7 @@ class EventAggregator {
160
173
  if (event.message.model) {
161
174
  this.currentModel = event.message.model;
162
175
  }
176
+ this.recordProviderMetadata(event);
163
177
  // 3. Skip system/audit events and synthetic token-count events for messageCount
164
178
  const msgId = event.message.id ?? '';
165
179
  if (event.type !== 'system' && !msgId.startsWith('token-count-')) {
@@ -171,7 +185,8 @@ class EventAggregator {
171
185
  }
172
186
  // 5. Token accumulation
173
187
  if (event.message.usage) {
174
- this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
188
+ const usageCost = this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
189
+ this.attributePlanUsage(event.message.usage.input_tokens + event.message.usage.output_tokens, usageCost);
175
190
  }
176
191
  if (event.type !== 'system') {
177
192
  // 6. Tool extraction from content blocks
@@ -187,6 +202,9 @@ class EventAggregator {
187
202
  this.attributeContextFromEvent(event);
188
203
  // 11. Permission mode tracking
189
204
  this.trackPermissionMode(event);
205
+ if (event.type === 'summary') {
206
+ this.recordExplicitCompaction(event.timestamp, event.compaction);
207
+ }
190
208
  // 12. Timeline
191
209
  this.addTimelineFromSessionEvent(event);
192
210
  // 13. Gonzo/Lazyjournal analytics
@@ -222,6 +240,7 @@ class EventAggregator {
222
240
  this.outputTokens += outputTok;
223
241
  this.cacheReadTokens += cacheRead;
224
242
  this.cacheWriteTokens += cacheWrite;
243
+ this.attributePlanUsage(inputTok + outputTok, event.cost ?? 0);
225
244
  // Tokens since last burn sample
226
245
  this.tokensSinceLastSample += inputTok + outputTok;
227
246
  // Context size
@@ -238,13 +257,17 @@ class EventAggregator {
238
257
  contextSize = inputTok + cacheWrite + cacheRead;
239
258
  }
240
259
  // Compaction detection
241
- if (this.previousContextSize > 0 &&
260
+ if (this.resolvePendingHeuristicCompaction(contextSize)) {
261
+ // The drop belongs to the explicit compaction marker just recorded.
262
+ }
263
+ else if (this.previousContextSize > 0 &&
242
264
  contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
243
265
  this.compactionEvents.push({
244
266
  timestamp: new Date(event.timestamp),
245
267
  contextBefore: this.previousContextSize,
246
268
  contextAfter: contextSize,
247
269
  tokensReclaimed: this.previousContextSize - contextSize,
270
+ source: 'heuristic',
248
271
  });
249
272
  }
250
273
  this.previousContextSize = contextSize;
@@ -297,6 +320,7 @@ class EventAggregator {
297
320
  // 4. Tool tracking from FollowEvent
298
321
  if (event.type === 'tool_use' && event.toolName) {
299
322
  this.recordFollowToolUse(event);
323
+ this.attributePlanToolCall(event.toolName);
300
324
  }
301
325
  else if (event.type === 'tool_result') {
302
326
  this.recordFollowToolResult(event);
@@ -306,7 +330,7 @@ class EventAggregator {
306
330
  // 6. Subagent tracking
307
331
  this.extractSubagentFromFollowEvent(event);
308
332
  // 7. Plan extraction
309
- this.planExtractor.processEvent(event);
333
+ this.processPlanFollowEvent(event);
310
334
  // 8. Context attribution
311
335
  this.attributeContextFromFollowEvent(event);
312
336
  // 9. Truncation detection from tool results
@@ -315,12 +339,7 @@ class EventAggregator {
315
339
  }
316
340
  // 10. Explicit compaction event (summary type)
317
341
  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
- });
342
+ this.recordExplicitCompaction(event.timestamp, event.compaction);
324
343
  }
325
344
  // 11. Timeline
326
345
  this.addTimelineFromFollowEvent(event);
@@ -354,6 +373,15 @@ class EventAggregator {
354
373
  b.pendingCount -
355
374
  (a.successCount + a.failureCount + a.pendingCount));
356
375
  }
376
+ getErrorRollup() {
377
+ return {
378
+ totalFailures: Array.from(this.errorByToolCategory.values()).reduce((sum, entry) => sum + entry.count, 0),
379
+ byToolCategory: Array.from(this.errorByToolCategory.values()).sort((a, b) => b.count - a.count),
380
+ byHourModel: Array.from(this.errorBuckets.values()).sort((a, b) => a.hour.localeCompare(b.hour)),
381
+ retryAttempts: this.retryAttempts,
382
+ finishReasons: Array.from(this.finishReasons, ([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count),
383
+ };
384
+ }
357
385
  getCompactionEvents() {
358
386
  return [...this.compactionEvents];
359
387
  }
@@ -379,10 +407,9 @@ class EventAggregator {
379
407
  return [...this.subagents];
380
408
  }
381
409
  getPlan() {
382
- const extracted = this.planExtractor.plan;
383
- if (!extracted)
384
- return null;
385
- return this.convertExtractedPlanToPlanState(extracted);
410
+ return this.planState
411
+ ? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
412
+ : null;
386
413
  }
387
414
  getContextAttribution() {
388
415
  return { ...this.contextAttribution };
@@ -424,6 +451,7 @@ class EventAggregator {
424
451
  truncationCount: this.truncationEvents.length,
425
452
  truncationEvents: this.getTruncationEvents(),
426
453
  toolStats: this.getToolStats(),
454
+ errorRollup: this.getErrorRollup(),
427
455
  burnRate: this.getBurnRate(),
428
456
  taskState: this.getTaskState(),
429
457
  subagents: this.getSubagents(),
@@ -455,6 +483,10 @@ class EventAggregator {
455
483
  this.truncationEvents = [];
456
484
  this.toolAnalytics.clear();
457
485
  this.pendingToolCalls.clear();
486
+ this.errorByToolCategory.clear();
487
+ this.errorBuckets.clear();
488
+ this.retryAttempts = 0;
489
+ this.finishReasons.clear();
458
490
  this.burnSamples = [];
459
491
  this.lastBurnSampleTime = 0;
460
492
  this.tokensSinceLastSample = 0;
@@ -464,6 +496,8 @@ class EventAggregator {
464
496
  this.subagents = [];
465
497
  this.pendingSubagents.clear();
466
498
  this.planExtractor.reset();
499
+ this.planState = null;
500
+ this.planEnteredAt = undefined;
467
501
  this.currentPermissionMode = null;
468
502
  this.permissionModeHistory = [];
469
503
  this.contextTimeline = [];
@@ -523,6 +557,10 @@ class EventAggregator {
523
557
  compactionEvents: this.compactionEvents,
524
558
  truncationEvents: this.truncationEvents,
525
559
  toolAnalytics: Array.from(this.toolAnalytics.entries()),
560
+ errorByToolCategory: Array.from(this.errorByToolCategory.entries()),
561
+ errorBuckets: Array.from(this.errorBuckets.entries()),
562
+ retryAttempts: this.retryAttempts,
563
+ finishReasons: Array.from(this.finishReasons.entries()),
526
564
  contextAttribution: { ...this.contextAttribution },
527
565
  burnSamples: [...this.burnSamples],
528
566
  lastBurnSampleTime: this.lastBurnSampleTime,
@@ -563,6 +601,10 @@ class EventAggregator {
563
601
  this.compactionEvents = [...state.compactionEvents];
564
602
  this.truncationEvents = [...state.truncationEvents];
565
603
  this.toolAnalytics = new Map(state.toolAnalytics);
604
+ this.errorByToolCategory = new Map(state.errorByToolCategory);
605
+ this.errorBuckets = new Map(state.errorBuckets);
606
+ this.retryAttempts = state.retryAttempts;
607
+ this.finishReasons = new Map(state.finishReasons);
566
608
  this.contextAttribution = { ...state.contextAttribution };
567
609
  this.burnSamples = [...state.burnSamples];
568
610
  this.lastBurnSampleTime = state.lastBurnSampleTime;
@@ -653,13 +695,17 @@ class EventAggregator {
653
695
  contextSize = inputTok + cacheWrite + cacheRead;
654
696
  }
655
697
  // Compaction detection
656
- if (this.previousContextSize > 0 &&
698
+ if (this.resolvePendingHeuristicCompaction(contextSize)) {
699
+ // The drop belongs to the explicit compaction marker just recorded.
700
+ }
701
+ else if (this.previousContextSize > 0 &&
657
702
  contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
658
703
  this.compactionEvents.push({
659
704
  timestamp: new Date(timestamp),
660
705
  contextBefore: this.previousContextSize,
661
706
  contextAfter: contextSize,
662
707
  tokensReclaimed: this.previousContextSize - contextSize,
708
+ source: 'heuristic',
663
709
  });
664
710
  }
665
711
  this.previousContextSize = contextSize;
@@ -697,6 +743,7 @@ class EventAggregator {
697
743
  this.modelUsage.set(perModelKey, acc);
698
744
  // Burn rate sampling
699
745
  this.updateBurnRate(timestamp);
746
+ return cost;
700
747
  }
701
748
  // ═══════════════════════════════════════════════════════════════════════
702
749
  // Private: Burn Rate
@@ -788,6 +835,7 @@ class EventAggregator {
788
835
  name,
789
836
  startTime: new Date(event.timestamp),
790
837
  });
838
+ this.attributePlanToolCall(name);
791
839
  }
792
840
  else if (block.type === 'tool_result') {
793
841
  // User content: tool_result block
@@ -797,6 +845,9 @@ class EventAggregator {
797
845
  continue;
798
846
  const pending = this.pendingToolCalls.get(toolUseId);
799
847
  if (pending) {
848
+ if (isError) {
849
+ this.recordError(pending.name, (0, errorTaxonomy_1.categorizeError)(block.content), event.timestamp, this.currentModel ?? 'unknown');
850
+ }
800
851
  this.pendingToolCalls.delete(toolUseId);
801
852
  const analytics = this.toolAnalytics.get(pending.name);
802
853
  if (analytics) {
@@ -890,6 +941,7 @@ class EventAggregator {
890
941
  const isError = raw.is_error === true;
891
942
  if (isError) {
892
943
  analytics.failureCount++;
944
+ this.recordError(pending.name, (0, errorTaxonomy_1.categorizeError)(raw.content ?? event.summary), event.timestamp, event.model ?? this.currentModel ?? 'unknown');
893
945
  }
894
946
  else {
895
947
  analytics.successCount++;
@@ -900,6 +952,36 @@ class EventAggregator {
900
952
  }
901
953
  }
902
954
  }
955
+ recordProviderMetadata(event) {
956
+ const metadata = event.providerMetadata;
957
+ if (!metadata)
958
+ return;
959
+ this.retryAttempts += metadata.retryAttempts?.length ?? 0;
960
+ for (const reason of metadata.finishReasons ?? []) {
961
+ this.finishReasons.set(reason, (this.finishReasons.get(reason) ?? 0) + 1);
962
+ }
963
+ if (metadata.providerError) {
964
+ this.recordError('ProviderAPI', (0, errorTaxonomy_1.categorizeError)(metadata.providerError, metadata.providerError.type), event.timestamp, event.message.model ?? this.currentModel ?? 'unknown');
965
+ }
966
+ }
967
+ recordError(tool, category, timestamp, model) {
968
+ const categoryKey = `${tool}\u0000${category}`;
969
+ const current = this.errorByToolCategory.get(categoryKey);
970
+ if (current)
971
+ current.count++;
972
+ else
973
+ this.errorByToolCategory.set(categoryKey, { tool, category, count: 1 });
974
+ const parsed = new Date(timestamp);
975
+ const hour = Number.isNaN(parsed.getTime())
976
+ ? timestamp.slice(0, 13)
977
+ : parsed.toISOString().slice(0, 13);
978
+ const bucketKey = `${hour}\u0000${model}\u0000${tool}\u0000${category}`;
979
+ const bucket = this.errorBuckets.get(bucketKey);
980
+ if (bucket)
981
+ bucket.count++;
982
+ else
983
+ this.errorBuckets.set(bucketKey, { hour, model, tool, category, count: 1 });
984
+ }
903
985
  detectTruncationFromFollowEvent(event) {
904
986
  const raw = event.raw;
905
987
  if (!raw)
@@ -936,6 +1018,49 @@ class EventAggregator {
936
1018
  }
937
1019
  }
938
1020
  }
1021
+ recordExplicitCompaction(timestamp, reported) {
1022
+ const hasReported = reported !== undefined &&
1023
+ Number.isFinite(reported.tokensBefore) &&
1024
+ Number.isFinite(reported.tokensAfter) &&
1025
+ reported.tokensBefore >= reported.tokensAfter &&
1026
+ reported.tokensAfter >= 0;
1027
+ // Without reported counts, assume the full context was reclaimed until
1028
+ // the next usage event supplies the real post-compaction size.
1029
+ const contextBefore = hasReported ? reported.tokensBefore : this.previousContextSize;
1030
+ const contextAfter = hasReported ? reported.tokensAfter : 0;
1031
+ this.compactionEvents.push({
1032
+ timestamp: new Date(timestamp),
1033
+ contextBefore,
1034
+ contextAfter,
1035
+ tokensReclaimed: Math.max(0, contextBefore - contextAfter),
1036
+ source: hasReported ? 'reported' : 'heuristic',
1037
+ });
1038
+ if (hasReported) {
1039
+ this.previousContextSize = contextAfter;
1040
+ this.currentContextSize = contextAfter;
1041
+ }
1042
+ else {
1043
+ this.pendingHeuristicCompactionIndex = this.compactionEvents.length - 1;
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Fills a pending heuristic compaction with the first observed
1048
+ * post-compaction context size. Returns true when the observed drop was
1049
+ * consumed by that pending event, so the caller must not record it again.
1050
+ */
1051
+ resolvePendingHeuristicCompaction(contextSize) {
1052
+ if (this.pendingHeuristicCompactionIndex === null)
1053
+ return false;
1054
+ const compaction = this.compactionEvents[this.pendingHeuristicCompactionIndex];
1055
+ this.pendingHeuristicCompactionIndex = null;
1056
+ if (!compaction)
1057
+ return false;
1058
+ if (contextSize < compaction.contextBefore) {
1059
+ compaction.contextAfter = contextSize;
1060
+ compaction.tokensReclaimed = compaction.contextBefore - contextSize;
1061
+ }
1062
+ return true;
1063
+ }
939
1064
  // ═══════════════════════════════════════════════════════════════════════
940
1065
  // Private: Task State from SessionEvent
941
1066
  // ═══════════════════════════════════════════════════════════════════════
@@ -962,6 +1087,7 @@ class EventAggregator {
962
1087
  }
963
1088
  else if (name === 'TaskUpdate') {
964
1089
  this.applyTaskUpdate(input);
1090
+ this.syncPlanStepFromTaskUpdate(input, new Date(event.timestamp));
965
1091
  }
966
1092
  else if (name === 'TodoWrite') {
967
1093
  this.handleTodoWrite(input, new Date(event.timestamp));
@@ -1005,6 +1131,7 @@ class EventAggregator {
1005
1131
  }
1006
1132
  else if (event.toolName === 'TaskUpdate') {
1007
1133
  this.applyTaskUpdate(input);
1134
+ this.syncPlanStepFromTaskUpdate(input, new Date(event.timestamp));
1008
1135
  }
1009
1136
  else if (event.toolName === 'TodoWrite') {
1010
1137
  this.handleTodoWrite(input, new Date(event.timestamp));
@@ -1102,6 +1229,36 @@ class EventAggregator {
1102
1229
  }
1103
1230
  }
1104
1231
  }
1232
+ syncPlanStepFromTaskUpdate(input, now) {
1233
+ if (!this.planState || !input.status)
1234
+ return;
1235
+ const taskId = String(input.taskId ?? input.task_id ?? '');
1236
+ const stepId = taskId.startsWith('plan-') ? `step-${taskId.slice(5)}` : taskId;
1237
+ const step = this.planState.steps.find((candidate) => candidate.id === stepId);
1238
+ if (!step)
1239
+ return;
1240
+ const rawStatus = String(input.status).toLowerCase();
1241
+ const status = rawStatus === 'completed'
1242
+ ? 'completed'
1243
+ : rawStatus === 'in_progress' || rawStatus === 'in-progress'
1244
+ ? 'in_progress'
1245
+ : rawStatus === 'deleted'
1246
+ ? 'skipped'
1247
+ : 'pending';
1248
+ if (status === 'in_progress' && step.status !== 'in_progress') {
1249
+ step.startedAt = now.toISOString();
1250
+ }
1251
+ if (status === 'completed' && step.status !== status) {
1252
+ step.startedAt ??= now.toISOString();
1253
+ step.completedAt = now.toISOString();
1254
+ step.durationMs = Math.max(0, now.getTime() - new Date(step.startedAt).getTime());
1255
+ }
1256
+ step.status = status;
1257
+ const completed = this.planState.steps.filter((candidate) => candidate.status === 'completed');
1258
+ this.planState.completionRate =
1259
+ this.planState.steps.length > 0 ? completed.length / this.planState.steps.length : 0;
1260
+ this.planState.active = this.planState.steps.some((candidate) => candidate.status === 'pending' || candidate.status === 'in_progress');
1261
+ }
1105
1262
  resolveTaskCreate(toolUseId, content, timestamp, isError) {
1106
1263
  const pending = this.pendingTaskCreates.get(toolUseId);
1107
1264
  if (!pending)
@@ -1408,27 +1565,83 @@ class EventAggregator {
1408
1565
  const providerId = this._providerId ?? 'claude-code';
1409
1566
  const followEvents = (0, eventBridge_1.toFollowEvents)(event, providerId);
1410
1567
  for (const fe of followEvents) {
1411
- this.planExtractor.processEvent(fe);
1568
+ this.processPlanFollowEvent(fe);
1412
1569
  }
1413
1570
  }
1414
- convertExtractedPlanToPlanState(extracted) {
1571
+ processPlanFollowEvent(event) {
1572
+ const now = new Date(event.timestamp);
1573
+ if (event.type === 'tool_use' && event.toolName === 'EnterPlanMode') {
1574
+ this.planEnteredAt = now;
1575
+ this.planState = {
1576
+ active: true,
1577
+ steps: [],
1578
+ enteredAt: now,
1579
+ source: event.providerId,
1580
+ };
1581
+ }
1582
+ const updated = this.planExtractor.processEvent(event);
1583
+ if (updated && this.planExtractor.plan) {
1584
+ this.planState = this.convertExtractedPlanToPlanState(this.planExtractor.plan, now);
1585
+ }
1586
+ if (event.type === 'tool_use' && event.toolName === 'ExitPlanMode' && this.planState) {
1587
+ this.planState.exitedAt = now;
1588
+ if (this.planState.enteredAt) {
1589
+ this.planState.totalDurationMs = now.getTime() - this.planState.enteredAt.getTime();
1590
+ }
1591
+ }
1592
+ }
1593
+ convertExtractedPlanToPlanState(extracted, now) {
1594
+ const previous = this.planState;
1595
+ const previousSteps = new Map((previous?.steps ?? []).map((step) => [`${step.id}\u0000${step.description}`, step]));
1415
1596
  const completed = extracted.steps.filter((s) => s.status === 'completed').length;
1416
1597
  const total = extracted.steps.length;
1598
+ const steps = extracted.steps.map((step) => {
1599
+ const old = previousSteps.get(`${step.id}\u0000${step.description}`);
1600
+ const merged = { ...step, ...old, status: step.status };
1601
+ if (step.status === 'in_progress' && old?.status !== 'in_progress' && !merged.startedAt) {
1602
+ merged.startedAt = now.toISOString();
1603
+ }
1604
+ if ((step.status === 'completed' || step.status === 'failed') &&
1605
+ old?.status !== step.status &&
1606
+ !merged.completedAt) {
1607
+ merged.startedAt ??= previous?.enteredAt?.toISOString() ?? now.toISOString();
1608
+ merged.completedAt = now.toISOString();
1609
+ merged.durationMs = Math.max(0, now.getTime() - new Date(merged.startedAt).getTime());
1610
+ }
1611
+ return merged;
1612
+ });
1417
1613
  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
- })),
1614
+ active: steps.some((step) => step.status === 'pending' || step.status === 'in_progress'),
1615
+ steps,
1426
1616
  title: extracted.title,
1427
1617
  source: extracted.source,
1428
1618
  completionRate: total > 0 ? completed / total : 0,
1429
1619
  rawMarkdown: extracted.rawMarkdown,
1620
+ enteredAt: previous?.enteredAt ?? this.planEnteredAt ?? now,
1621
+ exitedAt: previous?.exitedAt,
1622
+ totalDurationMs: previous?.totalDurationMs,
1623
+ prompt: previous?.prompt,
1624
+ revision: previous?.revision,
1430
1625
  };
1431
1626
  }
1627
+ getActivePlanStep() {
1628
+ return this.planState?.steps.find((step) => step.status === 'in_progress');
1629
+ }
1630
+ attributePlanUsage(tokens, costUsd) {
1631
+ const step = this.getActivePlanStep();
1632
+ if (!step)
1633
+ return;
1634
+ step.tokensUsed = (step.tokensUsed ?? 0) + tokens;
1635
+ if (costUsd > 0)
1636
+ step.costUsd = (step.costUsd ?? 0) + costUsd;
1637
+ }
1638
+ attributePlanToolCall(toolName) {
1639
+ if (['EnterPlanMode', 'ExitPlanMode', 'UpdatePlan', 'TaskUpdate'].includes(toolName))
1640
+ return;
1641
+ const step = this.getActivePlanStep();
1642
+ if (step)
1643
+ step.toolCalls = (step.toolCalls ?? 0) + 1;
1644
+ }
1432
1645
  // ═══════════════════════════════════════════════════════════════════════
1433
1646
  // Private: Context Attribution from SessionEvent
1434
1647
  // ═══════════════════════════════════════════════════════════════════════
@@ -51,7 +51,7 @@ const fs = __importStar(require("fs"));
51
51
  const path = __importStar(require("path"));
52
52
  const paths_1 = require("../paths");
53
53
  /** Current schema version — bump when format changes. */
54
- const SNAPSHOT_VERSION = 1;
54
+ const SNAPSHOT_VERSION = 3;
55
55
  /** Returns the directory for snapshot files. */
56
56
  function getSnapshotsDir() {
57
57
  return path.join((0, paths_1.getConfigDir)(), 'snapshots');
@@ -2,8 +2,29 @@
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). */
@@ -105,6 +126,7 @@ export interface AggregatedMetrics {
105
126
  truncationCount: number;
106
127
  truncationEvents: TruncationEvent[];
107
128
  toolStats: ToolAnalytics[];
129
+ errorRollup: ErrorRollup;
108
130
  burnRate: BurnRateInfo;
109
131
  taskState: TaskState;
110
132
  subagents: SubagentLifecycle[];
@@ -0,0 +1,20 @@
1
+ export interface ModelCostInput {
2
+ model: string;
3
+ cost: number;
4
+ priced?: boolean;
5
+ }
6
+ export interface CodeImpact {
7
+ changedLines: number;
8
+ additions: number;
9
+ deletions: number;
10
+ totalCost: number;
11
+ costPerChangedLine: number | null;
12
+ byModel: Array<{
13
+ model: string;
14
+ cost: number;
15
+ priced: boolean;
16
+ changedLines: number;
17
+ costPerChangedLine: number | null;
18
+ }>;
19
+ }
20
+ export declare function calculateCodeImpact(totalCost: number, additions: number, deletions: number, models?: ModelCostInput[]): CodeImpact;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateCodeImpact = calculateCodeImpact;
4
+ function calculateCodeImpact(totalCost, additions, deletions, models = []) {
5
+ const safeAdditions = Math.max(0, additions);
6
+ const safeDeletions = Math.max(0, deletions);
7
+ const changedLines = safeAdditions + safeDeletions;
8
+ return {
9
+ changedLines,
10
+ additions: safeAdditions,
11
+ deletions: safeDeletions,
12
+ totalCost,
13
+ costPerChangedLine: changedLines > 0 ? totalCost / changedLines : null,
14
+ byModel: models.map((model) => ({
15
+ ...model,
16
+ priced: model.priced !== false,
17
+ changedLines,
18
+ costPerChangedLine: changedLines > 0 && model.priced !== false ? model.cost / changedLines : null,
19
+ })),
20
+ };
21
+ }
@@ -0,0 +1,11 @@
1
+ import type { CompactionEvent } from '../types/sessionEvent';
2
+ export interface CompactionLedger {
3
+ count: number;
4
+ tokensEvicted: number;
5
+ reestablishmentCostUsd: number | null;
6
+ source: 'reported' | 'heuristic' | 'mixed';
7
+ reportedCount: number;
8
+ heuristicCount: number;
9
+ }
10
+ export declare function calculateCompactionLedger(events: CompactionEvent[], model?: string | null): CompactionLedger;
11
+ export declare function formatCompactionLedger(ledger: CompactionLedger): string;
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateCompactionLedger = calculateCompactionLedger;
4
+ exports.formatCompactionLedger = formatCompactionLedger;
5
+ const modelInfo_1 = require("../modelInfo");
6
+ function calculateCompactionLedger(events, model) {
7
+ const tokensEvicted = events.reduce((sum, event) => sum + Math.max(0, event.tokensReclaimed), 0);
8
+ const reportedCount = events.filter((event) => event.source === 'reported').length;
9
+ const heuristicCount = events.length - reportedCount;
10
+ const pricing = model ? (0, modelInfo_1.getModelPricing)(model) : null;
11
+ return {
12
+ count: events.length,
13
+ tokensEvicted,
14
+ reestablishmentCostUsd: pricing
15
+ ? (0, modelInfo_1.calculateCostWithPricing)({ inputTokens: tokensEvicted, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0 }, pricing)
16
+ : null,
17
+ source: reportedCount > 0 && heuristicCount > 0
18
+ ? 'mixed'
19
+ : reportedCount > 0
20
+ ? 'reported'
21
+ : 'heuristic',
22
+ reportedCount,
23
+ heuristicCount,
24
+ };
25
+ }
26
+ function formatCompactionLedger(ledger) {
27
+ const cost = ledger.reestablishmentCostUsd == null
28
+ ? 'cost unavailable'
29
+ : `~$${ledger.reestablishmentCostUsd.toFixed(2)} re-establishing context`;
30
+ return `${ledger.count} compaction${ledger.count === 1 ? '' : 's'} · ${ledger.tokensEvicted.toLocaleString()} tokens evicted · ${cost} · ${ledger.source}`;
31
+ }
@@ -0,0 +1,26 @@
1
+ import type { AggregatedMetrics } from '../aggregation/types';
2
+ import type { SessionHistoryRecord } from '../types/historicalData';
3
+ export type QualityFactorId = 'latency' | 'reliability' | 'context' | 'permission_safety' | 'outcomes';
4
+ export interface QualityFactorContribution {
5
+ id: QualityFactorId;
6
+ label: string;
7
+ contribution: number;
8
+ maximum: number;
9
+ detail: string;
10
+ }
11
+ export interface SessionQualityScore {
12
+ /** Composite score. Beta until weights are calibrated against durable history. */
13
+ score: number;
14
+ beta: true;
15
+ factors: QualityFactorContribution[];
16
+ }
17
+ export interface QualityTrend {
18
+ currentWeekAverage: number | null;
19
+ previousWeekAverage: number | null;
20
+ delta: number | null;
21
+ direction: 'improving' | 'declining' | 'flat' | 'insufficient';
22
+ currentWeekSessions: number;
23
+ previousWeekSessions: number;
24
+ }
25
+ export declare function scoreSessionQuality(metrics: AggregatedMetrics): SessionQualityScore;
26
+ export declare function calculateQualityTrend(records: SessionHistoryRecord[], now?: Date): QualityTrend;