sidekick-shared 0.22.0 → 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.
- package/README.md +51 -43
- package/dist/accountRegistry.d.ts +2 -0
- package/dist/accountStatus.d.ts +1 -0
- package/dist/accountStatus.js +6 -0
- package/dist/accounts.js +2 -0
- package/dist/aggregation/EventAggregator.d.ts +32 -1
- package/dist/aggregation/EventAggregator.js +238 -25
- package/dist/aggregation/snapshot.js +1 -1
- package/dist/aggregation/types.d.ts +23 -1
- package/dist/analytics/codeImpact.d.ts +20 -0
- package/dist/analytics/codeImpact.js +21 -0
- package/dist/analytics/compactionLedger.d.ts +11 -0
- package/dist/analytics/compactionLedger.js +31 -0
- package/dist/analytics/qualityScore.d.ts +26 -0
- package/dist/analytics/qualityScore.js +115 -0
- package/dist/codexProfiles.js +2 -0
- package/dist/context/composer.d.ts +3 -2
- package/dist/context/composer.js +5 -5
- package/dist/doctor.d.ts +55 -0
- package/dist/doctor.js +244 -0
- package/dist/errorHistory.d.ts +32 -0
- package/dist/errorHistory.js +94 -0
- package/dist/extractors/errorTaxonomy.d.ts +6 -0
- package/dist/extractors/errorTaxonomy.js +47 -0
- package/dist/extractors/toolCall.d.ts +9 -0
- package/dist/extractors/toolCall.js +79 -0
- package/dist/handoffUrl.d.ts +7 -0
- package/dist/handoffUrl.js +26 -0
- package/dist/index.d.ts +32 -9
- package/dist/index.js +62 -24
- package/dist/parsers/codexParser.js +13 -0
- package/dist/parsers/openCodeParser.js +13 -0
- package/dist/parsers/planExtractor.js +17 -6
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +18 -5
- package/dist/projectMigration.d.ts +23 -0
- package/dist/projectMigration.js +95 -0
- package/dist/providers/codexDatabase.d.ts +1 -11
- package/dist/providers/openCodeDatabase.d.ts +1 -31
- package/dist/providers/openCodeDatabase.js +9 -8
- package/dist/providers/types.d.ts +1 -0
- package/dist/readers/decisions.d.ts +2 -1
- package/dist/readers/decisions.js +2 -4
- package/dist/readers/handoff.d.ts +2 -1
- package/dist/readers/handoff.js +13 -8
- package/dist/readers/helpers.d.ts +3 -0
- package/dist/readers/helpers.js +12 -0
- package/dist/readers/notes.d.ts +2 -1
- package/dist/readers/notes.js +2 -4
- package/dist/readers/plans.js +1 -1
- package/dist/readers/tasks.d.ts +2 -1
- package/dist/readers/tasks.js +2 -4
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/observedSessionV1.d.ts +499 -0
- package/dist/schemas/observedSessionV1.js +69 -0
- package/dist/schemas/sessionEvent.d.ts +13 -0
- package/dist/schemas/sessionEvent.js +19 -0
- package/dist/search/sessionSearch.d.ts +2 -2
- package/dist/search/sessionSearch.js +2 -3
- package/dist/sessionMonitor.d.ts +29 -0
- package/dist/sessionMonitor.js +71 -0
- package/dist/statusline/BurnRateCalculator.d.ts +14 -0
- package/dist/statusline/BurnRateCalculator.js +45 -0
- package/dist/statusline/formatter.d.ts +16 -0
- package/dist/statusline/formatter.js +77 -0
- package/dist/statusline/index.d.ts +7 -0
- package/dist/statusline/index.js +13 -0
- package/dist/types/codex.d.ts +2 -0
- package/dist/types/historicalData.d.ts +35 -1
- package/dist/types/historicalData.js +6 -2
- package/dist/types/observedSessionV1.d.ts +70 -0
- package/dist/types/observedSessionV1.js +185 -0
- package/dist/types/opencode.d.ts +1 -0
- package/dist/types/sessionEvent.d.ts +18 -1
- package/dist/watchers/eventBridge.js +1 -0
- package/dist/watchers/types.d.ts +5 -0
- package/dist/writers/atomic.d.ts +4 -0
- package/dist/writers/atomic.js +113 -0
- package/dist/writers/decisions.d.ts +12 -0
- package/dist/writers/decisions.js +88 -0
- package/dist/writers/notes.d.ts +8 -0
- package/dist/writers/notes.js +82 -0
- package/dist/writers/tasks.d.ts +6 -0
- package/dist/writers/tasks.js +100 -0
- 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 =
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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.
|
|
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.
|
|
1568
|
+
this.processPlanFollowEvent(fe);
|
|
1412
1569
|
}
|
|
1413
1570
|
}
|
|
1414
|
-
|
|
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:
|
|
1419
|
-
steps
|
|
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 =
|
|
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,
|
|
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;
|