sidekick-shared 0.13.2
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 +92 -0
- package/dist/aggregation/EventAggregator.d.ts +172 -0
- package/dist/aggregation/EventAggregator.js +1443 -0
- package/dist/aggregation/FrequencyTracker.d.ts +42 -0
- package/dist/aggregation/FrequencyTracker.js +73 -0
- package/dist/aggregation/HeatmapTracker.d.ts +40 -0
- package/dist/aggregation/HeatmapTracker.js +93 -0
- package/dist/aggregation/PatternExtractor.d.ts +51 -0
- package/dist/aggregation/PatternExtractor.js +171 -0
- package/dist/aggregation/snapshot.d.ts +64 -0
- package/dist/aggregation/snapshot.js +151 -0
- package/dist/aggregation/types.d.ts +121 -0
- package/dist/aggregation/types.js +6 -0
- package/dist/context/composer.d.ts +31 -0
- package/dist/context/composer.js +72 -0
- package/dist/credentials.d.ts +23 -0
- package/dist/credentials.js +96 -0
- package/dist/formatters/eventHighlighter.d.ts +30 -0
- package/dist/formatters/eventHighlighter.js +217 -0
- package/dist/formatters/noiseClassifier.d.ts +73 -0
- package/dist/formatters/noiseClassifier.js +226 -0
- package/dist/formatters/sessionDump.d.ts +38 -0
- package/dist/formatters/sessionDump.js +313 -0
- package/dist/formatters/toolSummary.d.ts +23 -0
- package/dist/formatters/toolSummary.js +230 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.js +182 -0
- package/dist/parsers/changelogParser.d.ts +25 -0
- package/dist/parsers/changelogParser.js +74 -0
- package/dist/parsers/codexParser.d.ts +76 -0
- package/dist/parsers/codexParser.js +653 -0
- package/dist/parsers/debugLogParser.d.ts +63 -0
- package/dist/parsers/debugLogParser.js +164 -0
- package/dist/parsers/jsonl.d.ts +45 -0
- package/dist/parsers/jsonl.js +57 -0
- package/dist/parsers/openCodeParser.d.ts +64 -0
- package/dist/parsers/openCodeParser.js +581 -0
- package/dist/parsers/planExtractor.d.ts +63 -0
- package/dist/parsers/planExtractor.js +330 -0
- package/dist/parsers/sessionActivityDetector.d.ts +31 -0
- package/dist/parsers/sessionActivityDetector.js +184 -0
- package/dist/parsers/sessionPathResolver.d.ts +230 -0
- package/dist/parsers/sessionPathResolver.js +753 -0
- package/dist/parsers/subagentScanner.d.ts +43 -0
- package/dist/parsers/subagentScanner.js +366 -0
- package/dist/parsers/subagentTraceParser.d.ts +58 -0
- package/dist/parsers/subagentTraceParser.js +346 -0
- package/dist/paths.d.ts +38 -0
- package/dist/paths.js +107 -0
- package/dist/phrases.d.ts +52 -0
- package/dist/phrases.js +1333 -0
- package/dist/providers/claudeCode.d.ts +48 -0
- package/dist/providers/claudeCode.js +465 -0
- package/dist/providers/codex.d.ts +57 -0
- package/dist/providers/codex.js +944 -0
- package/dist/providers/codexDatabase.d.ts +37 -0
- package/dist/providers/codexDatabase.js +148 -0
- package/dist/providers/detect.d.ts +16 -0
- package/dist/providers/detect.js +162 -0
- package/dist/providers/openCode.d.ts +70 -0
- package/dist/providers/openCode.js +1524 -0
- package/dist/providers/openCodeDatabase.d.ts +87 -0
- package/dist/providers/openCodeDatabase.js +232 -0
- package/dist/providers/types.d.ts +154 -0
- package/dist/providers/types.js +12 -0
- package/dist/quota.d.ts +34 -0
- package/dist/quota.js +80 -0
- package/dist/readers/decisions.d.ts +10 -0
- package/dist/readers/decisions.js +27 -0
- package/dist/readers/handoff.d.ts +4 -0
- package/dist/readers/handoff.js +51 -0
- package/dist/readers/helpers.d.ts +7 -0
- package/dist/readers/helpers.js +52 -0
- package/dist/readers/history.d.ts +5 -0
- package/dist/readers/history.js +12 -0
- package/dist/readers/notes.d.ts +10 -0
- package/dist/readers/notes.js +46 -0
- package/dist/readers/plans.d.ts +35 -0
- package/dist/readers/plans.js +247 -0
- package/dist/readers/tasks.d.ts +8 -0
- package/dist/readers/tasks.js +22 -0
- package/dist/report/htmlHelpers.d.ts +18 -0
- package/dist/report/htmlHelpers.js +166 -0
- package/dist/report/htmlReportGenerator.d.ts +11 -0
- package/dist/report/htmlReportGenerator.js +650 -0
- package/dist/report/index.d.ts +8 -0
- package/dist/report/index.js +16 -0
- package/dist/report/logo.d.ts +2 -0
- package/dist/report/logo.js +5 -0
- package/dist/report/openBrowser.d.ts +5 -0
- package/dist/report/openBrowser.js +22 -0
- package/dist/report/transcriptParser.d.ts +12 -0
- package/dist/report/transcriptParser.js +177 -0
- package/dist/report/types.d.ts +43 -0
- package/dist/report/types.js +5 -0
- package/dist/search/advancedFilter.d.ts +62 -0
- package/dist/search/advancedFilter.js +201 -0
- package/dist/search/sessionSearch.d.ts +16 -0
- package/dist/search/sessionSearch.js +93 -0
- package/dist/types/codex.d.ts +276 -0
- package/dist/types/codex.js +14 -0
- package/dist/types/decisionLog.d.ts +23 -0
- package/dist/types/decisionLog.js +8 -0
- package/dist/types/historicalData.d.ts +74 -0
- package/dist/types/historicalData.js +17 -0
- package/dist/types/knowledgeNote.d.ts +40 -0
- package/dist/types/knowledgeNote.js +18 -0
- package/dist/types/opencode.d.ts +268 -0
- package/dist/types/opencode.js +13 -0
- package/dist/types/plan.d.ts +49 -0
- package/dist/types/plan.js +10 -0
- package/dist/types/sessionEvent.d.ts +562 -0
- package/dist/types/sessionEvent.js +11 -0
- package/dist/types/taskPersistence.d.ts +33 -0
- package/dist/types/taskPersistence.js +16 -0
- package/dist/watchers/eventBridge.d.ts +19 -0
- package/dist/watchers/eventBridge.js +162 -0
- package/dist/watchers/factory.d.ts +15 -0
- package/dist/watchers/factory.js +85 -0
- package/dist/watchers/jsonlWatcher.d.ts +30 -0
- package/dist/watchers/jsonlWatcher.js +444 -0
- package/dist/watchers/sqliteWatcher.d.ts +30 -0
- package/dist/watchers/sqliteWatcher.js +278 -0
- package/dist/watchers/types.d.ts +60 -0
- package/dist/watchers/types.js +5 -0
- package/package.json +31 -0
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared event aggregation engine.
|
|
4
|
+
*
|
|
5
|
+
* Pure computation class (no I/O, no framework dependencies) that processes
|
|
6
|
+
* SessionEvent and FollowEvent objects to accumulate session metrics.
|
|
7
|
+
* Both the VS Code SessionMonitor and CLI DashboardState delegate to this.
|
|
8
|
+
*
|
|
9
|
+
* @module aggregation/EventAggregator
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.EventAggregator = void 0;
|
|
13
|
+
const jsonl_1 = require("../parsers/jsonl");
|
|
14
|
+
const planExtractor_1 = require("../parsers/planExtractor");
|
|
15
|
+
const eventBridge_1 = require("../watchers/eventBridge");
|
|
16
|
+
const toolSummary_1 = require("../formatters/toolSummary");
|
|
17
|
+
const noiseClassifier_1 = require("../formatters/noiseClassifier");
|
|
18
|
+
const FrequencyTracker_1 = require("./FrequencyTracker");
|
|
19
|
+
const HeatmapTracker_1 = require("./HeatmapTracker");
|
|
20
|
+
const PatternExtractor_1 = require("./PatternExtractor");
|
|
21
|
+
// ── Defaults ──
|
|
22
|
+
const DEFAULT_TIMELINE_CAP = 200;
|
|
23
|
+
const DEFAULT_LATENCY_CAP = 100;
|
|
24
|
+
const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
|
|
25
|
+
const DEFAULT_BURN_SAMPLE_MS = 10_000;
|
|
26
|
+
const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
|
|
27
|
+
/** Schema version for serialized snapshots. */
|
|
28
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
29
|
+
// ── EventAggregator ──
|
|
30
|
+
class EventAggregator {
|
|
31
|
+
// Options
|
|
32
|
+
timelineCap;
|
|
33
|
+
latencyCap;
|
|
34
|
+
burnWindowMs;
|
|
35
|
+
burnSampleMs;
|
|
36
|
+
computeContextSize;
|
|
37
|
+
providerId;
|
|
38
|
+
// Token totals
|
|
39
|
+
inputTokens = 0;
|
|
40
|
+
outputTokens = 0;
|
|
41
|
+
cacheWriteTokens = 0;
|
|
42
|
+
cacheReadTokens = 0;
|
|
43
|
+
reportedCost = 0;
|
|
44
|
+
// Per-model usage
|
|
45
|
+
modelUsage = new Map();
|
|
46
|
+
// Context size tracking
|
|
47
|
+
currentContextSize = 0;
|
|
48
|
+
previousContextSize = 0;
|
|
49
|
+
// Compaction
|
|
50
|
+
compactionEvents = [];
|
|
51
|
+
// Truncation
|
|
52
|
+
truncationEvents = [];
|
|
53
|
+
// Tool analytics
|
|
54
|
+
toolAnalytics = new Map();
|
|
55
|
+
pendingToolCalls = new Map();
|
|
56
|
+
// Burn rate
|
|
57
|
+
burnSamples = [];
|
|
58
|
+
lastBurnSampleTime = 0;
|
|
59
|
+
tokensSinceLastSample = 0;
|
|
60
|
+
// Task state
|
|
61
|
+
tasks = new Map();
|
|
62
|
+
pendingTaskCreates = new Map();
|
|
63
|
+
activeTaskId = null;
|
|
64
|
+
// Subagent lifecycle
|
|
65
|
+
subagents = [];
|
|
66
|
+
pendingSubagents = new Map(); // toolUseId -> index
|
|
67
|
+
// Plan state
|
|
68
|
+
planExtractor;
|
|
69
|
+
// Context attribution
|
|
70
|
+
contextAttribution = {
|
|
71
|
+
systemPrompt: 0,
|
|
72
|
+
userMessages: 0,
|
|
73
|
+
assistantResponses: 0,
|
|
74
|
+
toolInputs: 0,
|
|
75
|
+
toolOutputs: 0,
|
|
76
|
+
thinking: 0,
|
|
77
|
+
other: 0,
|
|
78
|
+
};
|
|
79
|
+
// Permission mode
|
|
80
|
+
currentPermissionMode = null;
|
|
81
|
+
permissionModeHistory = [];
|
|
82
|
+
// Context timeline
|
|
83
|
+
contextTimeline = [];
|
|
84
|
+
contextTurnIndex = 0;
|
|
85
|
+
// Timeline
|
|
86
|
+
timeline = [];
|
|
87
|
+
// Latency tracking
|
|
88
|
+
pendingUserRequest = null;
|
|
89
|
+
latencyRecords = [];
|
|
90
|
+
// Counters
|
|
91
|
+
messageCount = 0;
|
|
92
|
+
eventCount = 0;
|
|
93
|
+
sessionStartTime = null;
|
|
94
|
+
lastEventTime = null;
|
|
95
|
+
currentModel = null;
|
|
96
|
+
_providerId = null;
|
|
97
|
+
// Gonzo/Lazyjournal analytics
|
|
98
|
+
toolFrequency = new FrequencyTracker_1.FrequencyTracker();
|
|
99
|
+
wordFrequency = new FrequencyTracker_1.FrequencyTracker();
|
|
100
|
+
patternExtractor = new PatternExtractor_1.PatternExtractor();
|
|
101
|
+
heatmapTracker = new HeatmapTracker_1.HeatmapTracker();
|
|
102
|
+
constructor(options) {
|
|
103
|
+
this.timelineCap = options?.timelineCap ?? DEFAULT_TIMELINE_CAP;
|
|
104
|
+
this.latencyCap = options?.latencyCap ?? DEFAULT_LATENCY_CAP;
|
|
105
|
+
this.burnWindowMs = options?.burnWindowMs ?? DEFAULT_BURN_WINDOW_MS;
|
|
106
|
+
this.burnSampleMs = options?.burnSampleMs ?? DEFAULT_BURN_SAMPLE_MS;
|
|
107
|
+
this.computeContextSize = options?.computeContextSize ?? null;
|
|
108
|
+
this.providerId = options?.providerId ?? null;
|
|
109
|
+
this._providerId = this.providerId;
|
|
110
|
+
this.planExtractor = new planExtractor_1.PlanExtractor(options?.readPlanFile);
|
|
111
|
+
}
|
|
112
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
113
|
+
// processEvent — main entry for SessionEvent
|
|
114
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
115
|
+
processEvent(event) {
|
|
116
|
+
// 1. Increment counters, capture timestamps
|
|
117
|
+
this.eventCount++;
|
|
118
|
+
if (!this.sessionStartTime) {
|
|
119
|
+
this.sessionStartTime = event.timestamp;
|
|
120
|
+
}
|
|
121
|
+
this.lastEventTime = event.timestamp;
|
|
122
|
+
// Guard: some event types (e.g. 'summary') have no message field in the raw JSONL
|
|
123
|
+
if (!event.message) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// 2. Track model
|
|
127
|
+
if (event.message.model) {
|
|
128
|
+
this.currentModel = event.message.model;
|
|
129
|
+
}
|
|
130
|
+
// 3. Skip synthetic token-count events for messageCount
|
|
131
|
+
const msgId = event.message.id ?? '';
|
|
132
|
+
if (!msgId.startsWith('token-count-')) {
|
|
133
|
+
this.messageCount++;
|
|
134
|
+
}
|
|
135
|
+
// 4. Latency tracking
|
|
136
|
+
this.processLatency(event);
|
|
137
|
+
// 5. Token accumulation
|
|
138
|
+
if (event.message.usage) {
|
|
139
|
+
this.accumulateUsage(event.message.usage, event.timestamp, event.message.model);
|
|
140
|
+
}
|
|
141
|
+
// 6. Tool extraction from content blocks
|
|
142
|
+
this.extractToolsFromContent(event);
|
|
143
|
+
// 7. Task state
|
|
144
|
+
this.extractTaskStateFromEvent(event);
|
|
145
|
+
// 8. Subagent tracking
|
|
146
|
+
this.extractSubagentFromEvent(event);
|
|
147
|
+
// 9. Plan extraction — convert SessionEvent to a FollowEvent shape for PlanExtractor
|
|
148
|
+
this.extractPlanFromSessionEvent(event);
|
|
149
|
+
// 10. Context attribution
|
|
150
|
+
this.attributeContextFromEvent(event);
|
|
151
|
+
// 11. Permission mode tracking
|
|
152
|
+
this.trackPermissionMode(event);
|
|
153
|
+
// 12. Timeline
|
|
154
|
+
this.addTimelineFromSessionEvent(event);
|
|
155
|
+
// 13. Gonzo/Lazyjournal analytics
|
|
156
|
+
const summary = this.extractTextContent(event) ?? '';
|
|
157
|
+
const toolName = this.extractToolNameFromEvent(event);
|
|
158
|
+
this.feedAnalytics(event.timestamp, summary, toolName);
|
|
159
|
+
}
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
161
|
+
// processFollowEvent — adapter for CLI FollowEvent format
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
163
|
+
processFollowEvent(event) {
|
|
164
|
+
// 1. Increment counters, capture timestamps
|
|
165
|
+
this.eventCount++;
|
|
166
|
+
if (!this.sessionStartTime) {
|
|
167
|
+
this.sessionStartTime = event.timestamp;
|
|
168
|
+
}
|
|
169
|
+
this.lastEventTime = event.timestamp;
|
|
170
|
+
// Provider
|
|
171
|
+
if (!this._providerId && event.providerId) {
|
|
172
|
+
this._providerId = event.providerId;
|
|
173
|
+
}
|
|
174
|
+
// 2. Model tracking
|
|
175
|
+
if (event.model) {
|
|
176
|
+
this.currentModel = event.model;
|
|
177
|
+
}
|
|
178
|
+
// 3. Token accumulation from FollowEvent fields
|
|
179
|
+
if (event.tokens) {
|
|
180
|
+
const inputTok = event.tokens.input;
|
|
181
|
+
const outputTok = event.tokens.output;
|
|
182
|
+
const cacheRead = event.cacheTokens?.read ?? 0;
|
|
183
|
+
const cacheWrite = event.cacheTokens?.write ?? 0;
|
|
184
|
+
this.inputTokens += inputTok;
|
|
185
|
+
this.outputTokens += outputTok;
|
|
186
|
+
this.cacheReadTokens += cacheRead;
|
|
187
|
+
this.cacheWriteTokens += cacheWrite;
|
|
188
|
+
// Tokens since last burn sample
|
|
189
|
+
this.tokensSinceLastSample += inputTok + outputTok;
|
|
190
|
+
// Context size
|
|
191
|
+
let contextSize;
|
|
192
|
+
if (this.computeContextSize) {
|
|
193
|
+
contextSize = this.computeContextSize({
|
|
194
|
+
inputTokens: inputTok,
|
|
195
|
+
outputTokens: outputTok,
|
|
196
|
+
cacheWriteTokens: cacheWrite,
|
|
197
|
+
cacheReadTokens: cacheRead,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
contextSize = inputTok + cacheWrite + cacheRead;
|
|
202
|
+
}
|
|
203
|
+
// Compaction detection
|
|
204
|
+
if (this.previousContextSize > 0 && contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
|
|
205
|
+
this.compactionEvents.push({
|
|
206
|
+
timestamp: new Date(event.timestamp),
|
|
207
|
+
contextBefore: this.previousContextSize,
|
|
208
|
+
contextAfter: contextSize,
|
|
209
|
+
tokensReclaimed: this.previousContextSize - contextSize,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
this.previousContextSize = contextSize;
|
|
213
|
+
this.currentContextSize = contextSize;
|
|
214
|
+
// Per-model usage
|
|
215
|
+
if (event.model) {
|
|
216
|
+
const model = event.model;
|
|
217
|
+
const acc = this.modelUsage.get(model) ?? {
|
|
218
|
+
calls: 0, tokens: 0, inputTokens: 0, outputTokens: 0,
|
|
219
|
+
cacheWriteTokens: 0, cacheReadTokens: 0, cost: 0,
|
|
220
|
+
};
|
|
221
|
+
acc.calls++;
|
|
222
|
+
acc.tokens += inputTok + outputTok;
|
|
223
|
+
acc.inputTokens += inputTok;
|
|
224
|
+
acc.outputTokens += outputTok;
|
|
225
|
+
acc.cacheWriteTokens += cacheWrite;
|
|
226
|
+
acc.cacheReadTokens += cacheRead;
|
|
227
|
+
if (event.cost)
|
|
228
|
+
acc.cost += event.cost;
|
|
229
|
+
this.modelUsage.set(model, acc);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Cost accumulation
|
|
233
|
+
if (event.cost) {
|
|
234
|
+
this.reportedCost += event.cost;
|
|
235
|
+
}
|
|
236
|
+
// Burn rate sampling
|
|
237
|
+
this.updateBurnRate(event.timestamp);
|
|
238
|
+
// 4. Tool tracking from FollowEvent
|
|
239
|
+
if (event.type === 'tool_use' && event.toolName) {
|
|
240
|
+
this.recordFollowToolUse(event);
|
|
241
|
+
}
|
|
242
|
+
else if (event.type === 'tool_result') {
|
|
243
|
+
this.recordFollowToolResult(event);
|
|
244
|
+
}
|
|
245
|
+
// 5. Task state extraction
|
|
246
|
+
this.extractTaskStateFromFollowEvent(event);
|
|
247
|
+
// 6. Subagent tracking
|
|
248
|
+
this.extractSubagentFromFollowEvent(event);
|
|
249
|
+
// 7. Plan extraction
|
|
250
|
+
this.planExtractor.processEvent(event);
|
|
251
|
+
// 8. Context attribution
|
|
252
|
+
this.attributeContextFromFollowEvent(event);
|
|
253
|
+
// 9. Truncation detection from tool results
|
|
254
|
+
if (event.type === 'tool_result') {
|
|
255
|
+
this.detectTruncationFromFollowEvent(event);
|
|
256
|
+
}
|
|
257
|
+
// 10. Explicit compaction event (summary type)
|
|
258
|
+
if (event.type === 'summary') {
|
|
259
|
+
this.compactionEvents.push({
|
|
260
|
+
timestamp: new Date(event.timestamp),
|
|
261
|
+
contextBefore: this.previousContextSize,
|
|
262
|
+
contextAfter: 0,
|
|
263
|
+
tokensReclaimed: this.previousContextSize,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
// 11. Timeline
|
|
267
|
+
this.addTimelineFromFollowEvent(event);
|
|
268
|
+
// 12. Gonzo/Lazyjournal analytics
|
|
269
|
+
this.feedAnalytics(event.timestamp, event.summary, event.toolName);
|
|
270
|
+
// Message count (skip system events)
|
|
271
|
+
if (event.type !== 'system') {
|
|
272
|
+
this.messageCount++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
276
|
+
// Public getters
|
|
277
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
278
|
+
getAggregatedTokens() {
|
|
279
|
+
return {
|
|
280
|
+
inputTokens: this.inputTokens,
|
|
281
|
+
outputTokens: this.outputTokens,
|
|
282
|
+
cacheWriteTokens: this.cacheWriteTokens,
|
|
283
|
+
cacheReadTokens: this.cacheReadTokens,
|
|
284
|
+
reportedCost: this.reportedCost,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
getModelStats() {
|
|
288
|
+
return Array.from(this.modelUsage.entries())
|
|
289
|
+
.map(([model, acc]) => ({ model, ...acc }))
|
|
290
|
+
.sort((a, b) => b.calls - a.calls);
|
|
291
|
+
}
|
|
292
|
+
getToolStats() {
|
|
293
|
+
return Array.from(this.toolAnalytics.values())
|
|
294
|
+
.sort((a, b) => (b.successCount + b.failureCount + b.pendingCount) - (a.successCount + a.failureCount + a.pendingCount));
|
|
295
|
+
}
|
|
296
|
+
getCompactionEvents() {
|
|
297
|
+
return [...this.compactionEvents];
|
|
298
|
+
}
|
|
299
|
+
getTruncationEvents() {
|
|
300
|
+
return [...this.truncationEvents];
|
|
301
|
+
}
|
|
302
|
+
getBurnRate() {
|
|
303
|
+
const points = this.burnSamples.map(s => s.tokens);
|
|
304
|
+
const tokensPerMinute = points.length > 0 ? points[points.length - 1] : 0;
|
|
305
|
+
return {
|
|
306
|
+
tokensPerMinute,
|
|
307
|
+
points: [...points],
|
|
308
|
+
sampleCount: this.burnSamples.length,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
getTaskState() {
|
|
312
|
+
return {
|
|
313
|
+
tasks: new Map(this.tasks),
|
|
314
|
+
activeTaskId: this.activeTaskId,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
getSubagents() {
|
|
318
|
+
return [...this.subagents];
|
|
319
|
+
}
|
|
320
|
+
getPlan() {
|
|
321
|
+
const extracted = this.planExtractor.plan;
|
|
322
|
+
if (!extracted)
|
|
323
|
+
return null;
|
|
324
|
+
return this.convertExtractedPlanToPlanState(extracted);
|
|
325
|
+
}
|
|
326
|
+
getContextAttribution() {
|
|
327
|
+
return { ...this.contextAttribution };
|
|
328
|
+
}
|
|
329
|
+
getTimeline() {
|
|
330
|
+
return [...this.timeline];
|
|
331
|
+
}
|
|
332
|
+
getLatencyStats() {
|
|
333
|
+
if (this.latencyRecords.length === 0)
|
|
334
|
+
return null;
|
|
335
|
+
const records = this.latencyRecords;
|
|
336
|
+
const totalFirstToken = records.reduce((sum, r) => sum + r.firstTokenLatencyMs, 0);
|
|
337
|
+
const totalResponse = records.reduce((sum, r) => sum + r.totalResponseTimeMs, 0);
|
|
338
|
+
const maxFirstToken = Math.max(...records.map(r => r.firstTokenLatencyMs));
|
|
339
|
+
const last = records[records.length - 1];
|
|
340
|
+
return {
|
|
341
|
+
recentLatencies: [...records],
|
|
342
|
+
avgFirstTokenLatencyMs: Math.round(totalFirstToken / records.length),
|
|
343
|
+
maxFirstTokenLatencyMs: maxFirstToken,
|
|
344
|
+
avgTotalResponseTimeMs: Math.round(totalResponse / records.length),
|
|
345
|
+
lastFirstTokenLatencyMs: last ? last.firstTokenLatencyMs : null,
|
|
346
|
+
completedCycles: records.length,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
getMetrics() {
|
|
350
|
+
return {
|
|
351
|
+
sessionStartTime: this.sessionStartTime,
|
|
352
|
+
lastEventTime: this.lastEventTime,
|
|
353
|
+
messageCount: this.messageCount,
|
|
354
|
+
eventCount: this.eventCount,
|
|
355
|
+
currentModel: this.currentModel,
|
|
356
|
+
providerId: this._providerId,
|
|
357
|
+
tokens: this.getAggregatedTokens(),
|
|
358
|
+
modelStats: this.getModelStats(),
|
|
359
|
+
currentContextSize: this.currentContextSize,
|
|
360
|
+
contextAttribution: this.getContextAttribution(),
|
|
361
|
+
compactionCount: this.compactionEvents.length,
|
|
362
|
+
compactionEvents: this.getCompactionEvents(),
|
|
363
|
+
truncationCount: this.truncationEvents.length,
|
|
364
|
+
truncationEvents: this.getTruncationEvents(),
|
|
365
|
+
toolStats: this.getToolStats(),
|
|
366
|
+
burnRate: this.getBurnRate(),
|
|
367
|
+
taskState: this.getTaskState(),
|
|
368
|
+
subagents: this.getSubagents(),
|
|
369
|
+
plan: this.getPlan(),
|
|
370
|
+
permissionMode: this.currentPermissionMode,
|
|
371
|
+
permissionModeHistory: [...this.permissionModeHistory],
|
|
372
|
+
contextTimeline: [...this.contextTimeline],
|
|
373
|
+
timeline: this.getTimeline(),
|
|
374
|
+
latencyStats: this.getLatencyStats(),
|
|
375
|
+
toolFrequency: this.toolFrequency.getTopN(20).map(e => ({ name: e.key, count: e.count })),
|
|
376
|
+
wordFrequency: this.wordFrequency.getTopN(20).map(e => ({ name: e.key, count: e.count })),
|
|
377
|
+
patterns: this.patternExtractor.getPatterns().slice(0, 20),
|
|
378
|
+
heatmapBuckets: this.heatmapTracker.getBuckets(),
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
382
|
+
// reset — clear all state
|
|
383
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
384
|
+
reset() {
|
|
385
|
+
this.inputTokens = 0;
|
|
386
|
+
this.outputTokens = 0;
|
|
387
|
+
this.cacheWriteTokens = 0;
|
|
388
|
+
this.cacheReadTokens = 0;
|
|
389
|
+
this.reportedCost = 0;
|
|
390
|
+
this.modelUsage.clear();
|
|
391
|
+
this.currentContextSize = 0;
|
|
392
|
+
this.previousContextSize = 0;
|
|
393
|
+
this.compactionEvents = [];
|
|
394
|
+
this.truncationEvents = [];
|
|
395
|
+
this.toolAnalytics.clear();
|
|
396
|
+
this.pendingToolCalls.clear();
|
|
397
|
+
this.burnSamples = [];
|
|
398
|
+
this.lastBurnSampleTime = 0;
|
|
399
|
+
this.tokensSinceLastSample = 0;
|
|
400
|
+
this.tasks.clear();
|
|
401
|
+
this.pendingTaskCreates.clear();
|
|
402
|
+
this.activeTaskId = null;
|
|
403
|
+
this.subagents = [];
|
|
404
|
+
this.pendingSubagents.clear();
|
|
405
|
+
this.planExtractor.reset();
|
|
406
|
+
this.currentPermissionMode = null;
|
|
407
|
+
this.permissionModeHistory = [];
|
|
408
|
+
this.contextTimeline = [];
|
|
409
|
+
this.contextTurnIndex = 0;
|
|
410
|
+
this.contextAttribution = {
|
|
411
|
+
systemPrompt: 0,
|
|
412
|
+
userMessages: 0,
|
|
413
|
+
assistantResponses: 0,
|
|
414
|
+
toolInputs: 0,
|
|
415
|
+
toolOutputs: 0,
|
|
416
|
+
thinking: 0,
|
|
417
|
+
other: 0,
|
|
418
|
+
};
|
|
419
|
+
this.timeline = [];
|
|
420
|
+
this.pendingUserRequest = null;
|
|
421
|
+
this.latencyRecords = [];
|
|
422
|
+
this.toolFrequency.reset();
|
|
423
|
+
this.wordFrequency.reset();
|
|
424
|
+
this.patternExtractor.reset();
|
|
425
|
+
this.heatmapTracker.reset();
|
|
426
|
+
this.messageCount = 0;
|
|
427
|
+
this.eventCount = 0;
|
|
428
|
+
this.sessionStartTime = null;
|
|
429
|
+
this.lastEventTime = null;
|
|
430
|
+
this.currentModel = null;
|
|
431
|
+
this._providerId = this.providerId;
|
|
432
|
+
}
|
|
433
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
434
|
+
// Seed methods — for initializing state from provider snapshots
|
|
435
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
436
|
+
/** Seeds the current context size (e.g., from a provider usage snapshot on attach). */
|
|
437
|
+
seedContextSize(size) {
|
|
438
|
+
this.currentContextSize = size;
|
|
439
|
+
this.previousContextSize = size;
|
|
440
|
+
}
|
|
441
|
+
/** Seeds context attribution (e.g., from a provider's DB-backed attribution). */
|
|
442
|
+
seedContextAttribution(attribution) {
|
|
443
|
+
this.contextAttribution = { ...attribution };
|
|
444
|
+
}
|
|
445
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
446
|
+
// Snapshot serialization — for fast session resume
|
|
447
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
448
|
+
/** Serializes all mutable state to a JSON-safe object for snapshot persistence. */
|
|
449
|
+
serialize() {
|
|
450
|
+
return {
|
|
451
|
+
version: SNAPSHOT_SCHEMA_VERSION,
|
|
452
|
+
tokens: {
|
|
453
|
+
input: this.inputTokens,
|
|
454
|
+
output: this.outputTokens,
|
|
455
|
+
cacheWrite: this.cacheWriteTokens,
|
|
456
|
+
cacheRead: this.cacheReadTokens,
|
|
457
|
+
reportedCost: this.reportedCost,
|
|
458
|
+
},
|
|
459
|
+
modelUsage: Array.from(this.modelUsage.entries()),
|
|
460
|
+
contextSize: this.currentContextSize,
|
|
461
|
+
previousContextSize: this.previousContextSize,
|
|
462
|
+
compactionEvents: this.compactionEvents,
|
|
463
|
+
truncationEvents: this.truncationEvents,
|
|
464
|
+
toolAnalytics: Array.from(this.toolAnalytics.entries()),
|
|
465
|
+
contextAttribution: { ...this.contextAttribution },
|
|
466
|
+
burnSamples: [...this.burnSamples],
|
|
467
|
+
lastBurnSampleTime: this.lastBurnSampleTime,
|
|
468
|
+
tokensSinceLastSample: this.tokensSinceLastSample,
|
|
469
|
+
latencyRecords: [...this.latencyRecords],
|
|
470
|
+
tasks: Array.from(this.tasks.entries()),
|
|
471
|
+
activeTaskId: this.activeTaskId,
|
|
472
|
+
subagents: [...this.subagents],
|
|
473
|
+
permissionMode: this.currentPermissionMode,
|
|
474
|
+
permissionModeHistory: [...this.permissionModeHistory],
|
|
475
|
+
contextTimeline: [...this.contextTimeline],
|
|
476
|
+
contextTurnIndex: this.contextTurnIndex,
|
|
477
|
+
timeline: [...this.timeline],
|
|
478
|
+
messageCount: this.messageCount,
|
|
479
|
+
eventCount: this.eventCount,
|
|
480
|
+
sessionStartTime: this.sessionStartTime,
|
|
481
|
+
lastEventTime: this.lastEventTime,
|
|
482
|
+
currentModel: this.currentModel,
|
|
483
|
+
toolFrequency: this.toolFrequency.serialize(),
|
|
484
|
+
wordFrequency: this.wordFrequency.serialize(),
|
|
485
|
+
patternState: this.patternExtractor.serialize(),
|
|
486
|
+
heatmapState: this.heatmapTracker.serialize(),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
/** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
|
|
490
|
+
restore(state) {
|
|
491
|
+
if (state.version !== SNAPSHOT_SCHEMA_VERSION) {
|
|
492
|
+
return; // Incompatible snapshot — caller should fall back to full replay
|
|
493
|
+
}
|
|
494
|
+
this.inputTokens = state.tokens.input;
|
|
495
|
+
this.outputTokens = state.tokens.output;
|
|
496
|
+
this.cacheWriteTokens = state.tokens.cacheWrite;
|
|
497
|
+
this.cacheReadTokens = state.tokens.cacheRead;
|
|
498
|
+
this.reportedCost = state.tokens.reportedCost;
|
|
499
|
+
this.modelUsage = new Map(state.modelUsage);
|
|
500
|
+
this.currentContextSize = state.contextSize;
|
|
501
|
+
this.previousContextSize = state.previousContextSize;
|
|
502
|
+
this.compactionEvents = [...state.compactionEvents];
|
|
503
|
+
this.truncationEvents = [...state.truncationEvents];
|
|
504
|
+
this.toolAnalytics = new Map(state.toolAnalytics);
|
|
505
|
+
this.contextAttribution = { ...state.contextAttribution };
|
|
506
|
+
this.burnSamples = [...state.burnSamples];
|
|
507
|
+
this.lastBurnSampleTime = state.lastBurnSampleTime;
|
|
508
|
+
this.tokensSinceLastSample = state.tokensSinceLastSample;
|
|
509
|
+
this.latencyRecords = [...state.latencyRecords];
|
|
510
|
+
this.tasks = new Map(state.tasks);
|
|
511
|
+
this.activeTaskId = state.activeTaskId;
|
|
512
|
+
this.subagents = [...state.subagents];
|
|
513
|
+
this.currentPermissionMode = state.permissionMode ?? null;
|
|
514
|
+
this.permissionModeHistory = state.permissionModeHistory ? [...state.permissionModeHistory] : [];
|
|
515
|
+
this.contextTimeline = state.contextTimeline ? [...state.contextTimeline] : [];
|
|
516
|
+
this.contextTurnIndex = state.contextTurnIndex ?? 0;
|
|
517
|
+
this.timeline = [...state.timeline];
|
|
518
|
+
this.messageCount = state.messageCount;
|
|
519
|
+
this.eventCount = state.eventCount;
|
|
520
|
+
this.sessionStartTime = state.sessionStartTime;
|
|
521
|
+
this.lastEventTime = state.lastEventTime;
|
|
522
|
+
this.currentModel = state.currentModel;
|
|
523
|
+
// Gonzo/Lazyjournal analytics restore
|
|
524
|
+
if (state.toolFrequency)
|
|
525
|
+
this.toolFrequency.restore(state.toolFrequency);
|
|
526
|
+
if (state.wordFrequency)
|
|
527
|
+
this.wordFrequency.restore(state.wordFrequency);
|
|
528
|
+
if (state.patternState)
|
|
529
|
+
this.patternExtractor.restore(state.patternState);
|
|
530
|
+
if (state.heatmapState)
|
|
531
|
+
this.heatmapTracker.restore(state.heatmapState);
|
|
532
|
+
// Clear transient state — pending calls won't survive a snapshot boundary
|
|
533
|
+
this.pendingToolCalls.clear();
|
|
534
|
+
this.pendingTaskCreates.clear();
|
|
535
|
+
this.pendingSubagents.clear();
|
|
536
|
+
this.pendingUserRequest = null;
|
|
537
|
+
this.planExtractor.reset();
|
|
538
|
+
}
|
|
539
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
540
|
+
// Private: Token & Context
|
|
541
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
542
|
+
accumulateUsage(usage, timestamp, model) {
|
|
543
|
+
const inputTok = usage.input_tokens;
|
|
544
|
+
const outputTok = usage.output_tokens;
|
|
545
|
+
const cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
546
|
+
const cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
547
|
+
const cost = usage.reported_cost ?? 0;
|
|
548
|
+
this.inputTokens += inputTok;
|
|
549
|
+
this.outputTokens += outputTok;
|
|
550
|
+
this.cacheWriteTokens += cacheWrite;
|
|
551
|
+
this.cacheReadTokens += cacheRead;
|
|
552
|
+
this.reportedCost += cost;
|
|
553
|
+
// Burn rate sample accumulation
|
|
554
|
+
this.tokensSinceLastSample += inputTok + outputTok;
|
|
555
|
+
// Context size computation
|
|
556
|
+
let contextSize;
|
|
557
|
+
if (this.computeContextSize) {
|
|
558
|
+
contextSize = this.computeContextSize({
|
|
559
|
+
inputTokens: inputTok,
|
|
560
|
+
outputTokens: outputTok,
|
|
561
|
+
cacheWriteTokens: cacheWrite,
|
|
562
|
+
cacheReadTokens: cacheRead,
|
|
563
|
+
reasoningTokens: usage.reasoning_tokens,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
contextSize = inputTok + cacheWrite + cacheRead;
|
|
568
|
+
}
|
|
569
|
+
// Compaction detection
|
|
570
|
+
if (this.previousContextSize > 0 && contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
|
|
571
|
+
this.compactionEvents.push({
|
|
572
|
+
timestamp: new Date(timestamp),
|
|
573
|
+
contextBefore: this.previousContextSize,
|
|
574
|
+
contextAfter: contextSize,
|
|
575
|
+
tokensReclaimed: this.previousContextSize - contextSize,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
this.previousContextSize = contextSize;
|
|
579
|
+
this.currentContextSize = contextSize;
|
|
580
|
+
// Context timeline tracking
|
|
581
|
+
this.contextTimeline.push({
|
|
582
|
+
timestamp,
|
|
583
|
+
inputTokens: contextSize,
|
|
584
|
+
turnIndex: this.contextTurnIndex++,
|
|
585
|
+
});
|
|
586
|
+
// Per-model usage
|
|
587
|
+
const modelKey = model ?? this.currentModel ?? 'unknown';
|
|
588
|
+
const acc = this.modelUsage.get(modelKey) ?? {
|
|
589
|
+
calls: 0, tokens: 0, inputTokens: 0, outputTokens: 0,
|
|
590
|
+
cacheWriteTokens: 0, cacheReadTokens: 0, cost: 0,
|
|
591
|
+
};
|
|
592
|
+
acc.calls++;
|
|
593
|
+
acc.tokens += inputTok + outputTok;
|
|
594
|
+
acc.inputTokens += inputTok;
|
|
595
|
+
acc.outputTokens += outputTok;
|
|
596
|
+
acc.cacheWriteTokens += cacheWrite;
|
|
597
|
+
acc.cacheReadTokens += cacheRead;
|
|
598
|
+
acc.cost += cost;
|
|
599
|
+
this.modelUsage.set(modelKey, acc);
|
|
600
|
+
// Burn rate sampling
|
|
601
|
+
this.updateBurnRate(timestamp);
|
|
602
|
+
}
|
|
603
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
604
|
+
// Private: Burn Rate
|
|
605
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
606
|
+
updateBurnRate(timestamp) {
|
|
607
|
+
const now = new Date(timestamp).getTime();
|
|
608
|
+
if (isNaN(now))
|
|
609
|
+
return;
|
|
610
|
+
if (this.lastBurnSampleTime === 0) {
|
|
611
|
+
this.lastBurnSampleTime = now;
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const elapsed = now - this.lastBurnSampleTime;
|
|
615
|
+
if (elapsed >= this.burnSampleMs) {
|
|
616
|
+
const tokPerMin = elapsed > 0 ? Math.round((this.tokensSinceLastSample / elapsed) * 60_000) : 0;
|
|
617
|
+
this.burnSamples.push({ time: now, tokens: tokPerMin });
|
|
618
|
+
this.tokensSinceLastSample = 0;
|
|
619
|
+
this.lastBurnSampleTime = now;
|
|
620
|
+
// Trim to window
|
|
621
|
+
const cutoff = now - this.burnWindowMs;
|
|
622
|
+
this.burnSamples = this.burnSamples.filter(s => s.time >= cutoff);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
626
|
+
// Private: Latency Tracking (SessionEvent only)
|
|
627
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
628
|
+
processLatency(event) {
|
|
629
|
+
const now = new Date(event.timestamp);
|
|
630
|
+
if (event.type === 'user' && this.hasTextContent(event)) {
|
|
631
|
+
// User event with text content -> start tracking
|
|
632
|
+
this.pendingUserRequest = {
|
|
633
|
+
timestamp: now,
|
|
634
|
+
firstResponseReceived: false,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
else if (event.type === 'assistant' && this.pendingUserRequest) {
|
|
638
|
+
if (!this.pendingUserRequest.firstResponseReceived && this.hasTextContent(event)) {
|
|
639
|
+
// First assistant response with text
|
|
640
|
+
this.pendingUserRequest.firstResponseReceived = true;
|
|
641
|
+
this.pendingUserRequest.firstResponseTimestamp = now;
|
|
642
|
+
this.pendingUserRequest.firstTokenLatencyMs = now.getTime() - this.pendingUserRequest.timestamp.getTime();
|
|
643
|
+
}
|
|
644
|
+
if (event.message.usage && this.pendingUserRequest.firstResponseReceived) {
|
|
645
|
+
// Assistant with usage -> complete the cycle
|
|
646
|
+
const totalResponseTimeMs = now.getTime() - this.pendingUserRequest.timestamp.getTime();
|
|
647
|
+
const firstTokenLatencyMs = this.pendingUserRequest.firstTokenLatencyMs ?? totalResponseTimeMs;
|
|
648
|
+
this.latencyRecords.push({
|
|
649
|
+
firstTokenLatencyMs,
|
|
650
|
+
totalResponseTimeMs,
|
|
651
|
+
requestTimestamp: this.pendingUserRequest.timestamp,
|
|
652
|
+
});
|
|
653
|
+
// Cap latency records
|
|
654
|
+
if (this.latencyRecords.length > this.latencyCap) {
|
|
655
|
+
this.latencyRecords.shift();
|
|
656
|
+
}
|
|
657
|
+
this.pendingUserRequest = null;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
662
|
+
// Private: Tool Extraction from SessionEvent content blocks
|
|
663
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
664
|
+
extractToolsFromContent(event) {
|
|
665
|
+
const content = event.message.content;
|
|
666
|
+
if (!Array.isArray(content))
|
|
667
|
+
return;
|
|
668
|
+
for (const block of content) {
|
|
669
|
+
if (block.type === 'tool_use') {
|
|
670
|
+
// Assistant content: tool_use block
|
|
671
|
+
const toolUseId = block.id;
|
|
672
|
+
const name = block.name;
|
|
673
|
+
if (!toolUseId || !name)
|
|
674
|
+
continue;
|
|
675
|
+
// Record in toolAnalytics
|
|
676
|
+
const analytics = this.toolAnalytics.get(name) ?? {
|
|
677
|
+
name, successCount: 0, failureCount: 0, totalDuration: 0, completedCount: 0, pendingCount: 0,
|
|
678
|
+
};
|
|
679
|
+
analytics.pendingCount++;
|
|
680
|
+
this.toolAnalytics.set(name, analytics);
|
|
681
|
+
// Track pending
|
|
682
|
+
this.pendingToolCalls.set(toolUseId, {
|
|
683
|
+
toolUseId,
|
|
684
|
+
name,
|
|
685
|
+
startTime: new Date(event.timestamp),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
else if (block.type === 'tool_result') {
|
|
689
|
+
// User content: tool_result block
|
|
690
|
+
const toolUseId = block.tool_use_id;
|
|
691
|
+
const isError = block.is_error === true;
|
|
692
|
+
if (!toolUseId)
|
|
693
|
+
continue;
|
|
694
|
+
const pending = this.pendingToolCalls.get(toolUseId);
|
|
695
|
+
if (pending) {
|
|
696
|
+
this.pendingToolCalls.delete(toolUseId);
|
|
697
|
+
const analytics = this.toolAnalytics.get(pending.name);
|
|
698
|
+
if (analytics) {
|
|
699
|
+
analytics.pendingCount = Math.max(0, analytics.pendingCount - 1);
|
|
700
|
+
analytics.completedCount++;
|
|
701
|
+
if (isError) {
|
|
702
|
+
analytics.failureCount++;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
analytics.successCount++;
|
|
706
|
+
}
|
|
707
|
+
// Duration
|
|
708
|
+
const duration = new Date(event.timestamp).getTime() - pending.startTime.getTime();
|
|
709
|
+
if (duration >= 0) {
|
|
710
|
+
analytics.totalDuration += duration;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
// Truncation detection
|
|
715
|
+
this.detectTruncationInContent(block, event.timestamp);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
detectTruncationInContent(block, timestamp) {
|
|
720
|
+
const content = block.content;
|
|
721
|
+
let text;
|
|
722
|
+
if (typeof content === 'string')
|
|
723
|
+
text = content;
|
|
724
|
+
else if (content)
|
|
725
|
+
text = JSON.stringify(content);
|
|
726
|
+
else
|
|
727
|
+
text = '';
|
|
728
|
+
if (!text)
|
|
729
|
+
return;
|
|
730
|
+
for (const pattern of jsonl_1.TRUNCATION_PATTERNS) {
|
|
731
|
+
if (pattern.regex.test(text)) {
|
|
732
|
+
// Try to find the tool name from pending calls
|
|
733
|
+
const toolUseId = block.tool_use_id;
|
|
734
|
+
const pending = toolUseId ? this.pendingToolCalls.get(toolUseId) : undefined;
|
|
735
|
+
this.truncationEvents.push({
|
|
736
|
+
timestamp: new Date(timestamp),
|
|
737
|
+
toolName: pending?.name ?? 'unknown',
|
|
738
|
+
marker: pattern.name,
|
|
739
|
+
});
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
745
|
+
// Private: Tool tracking from FollowEvent
|
|
746
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
747
|
+
recordFollowToolUse(event) {
|
|
748
|
+
const name = event.toolName;
|
|
749
|
+
// Update analytics
|
|
750
|
+
const analytics = this.toolAnalytics.get(name) ?? {
|
|
751
|
+
name, successCount: 0, failureCount: 0, totalDuration: 0, completedCount: 0, pendingCount: 0,
|
|
752
|
+
};
|
|
753
|
+
analytics.pendingCount++;
|
|
754
|
+
this.toolAnalytics.set(name, analytics);
|
|
755
|
+
// Track pending by tool_use_id from raw
|
|
756
|
+
const raw = event.raw;
|
|
757
|
+
const toolUseId = raw?.id;
|
|
758
|
+
if (toolUseId) {
|
|
759
|
+
this.pendingToolCalls.set(toolUseId, {
|
|
760
|
+
toolUseId,
|
|
761
|
+
name,
|
|
762
|
+
startTime: new Date(event.timestamp),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
recordFollowToolResult(event) {
|
|
767
|
+
const raw = event.raw;
|
|
768
|
+
if (!raw)
|
|
769
|
+
return;
|
|
770
|
+
const toolUseId = raw.tool_use_id;
|
|
771
|
+
if (!toolUseId)
|
|
772
|
+
return;
|
|
773
|
+
const pending = this.pendingToolCalls.get(toolUseId);
|
|
774
|
+
if (!pending)
|
|
775
|
+
return;
|
|
776
|
+
this.pendingToolCalls.delete(toolUseId);
|
|
777
|
+
const analytics = this.toolAnalytics.get(pending.name);
|
|
778
|
+
if (analytics) {
|
|
779
|
+
analytics.pendingCount = Math.max(0, analytics.pendingCount - 1);
|
|
780
|
+
analytics.completedCount++;
|
|
781
|
+
const isError = raw.is_error === true;
|
|
782
|
+
if (isError) {
|
|
783
|
+
analytics.failureCount++;
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
analytics.successCount++;
|
|
787
|
+
}
|
|
788
|
+
const duration = new Date(event.timestamp).getTime() - pending.startTime.getTime();
|
|
789
|
+
if (duration >= 0) {
|
|
790
|
+
analytics.totalDuration += duration;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
detectTruncationFromFollowEvent(event) {
|
|
795
|
+
const raw = event.raw;
|
|
796
|
+
if (!raw)
|
|
797
|
+
return;
|
|
798
|
+
const content = raw.content;
|
|
799
|
+
let text;
|
|
800
|
+
if (typeof content === 'string')
|
|
801
|
+
text = content;
|
|
802
|
+
else if (content)
|
|
803
|
+
text = JSON.stringify(content);
|
|
804
|
+
else
|
|
805
|
+
text = '';
|
|
806
|
+
if (!text)
|
|
807
|
+
return;
|
|
808
|
+
for (const pattern of jsonl_1.TRUNCATION_PATTERNS) {
|
|
809
|
+
if (pattern.regex.test(text)) {
|
|
810
|
+
const toolUseId = raw.tool_use_id;
|
|
811
|
+
// Try to find the tool name — it may have been resolved already, so check summary
|
|
812
|
+
let toolName = 'unknown';
|
|
813
|
+
if (toolUseId) {
|
|
814
|
+
const pending = this.pendingToolCalls.get(toolUseId);
|
|
815
|
+
if (pending)
|
|
816
|
+
toolName = pending.name;
|
|
817
|
+
}
|
|
818
|
+
if (toolName === 'unknown' && event.toolName) {
|
|
819
|
+
toolName = event.toolName;
|
|
820
|
+
}
|
|
821
|
+
this.truncationEvents.push({
|
|
822
|
+
timestamp: new Date(event.timestamp),
|
|
823
|
+
toolName,
|
|
824
|
+
marker: pattern.name,
|
|
825
|
+
});
|
|
826
|
+
break;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
831
|
+
// Private: Task State from SessionEvent
|
|
832
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
833
|
+
extractTaskStateFromEvent(event) {
|
|
834
|
+
const content = event.message.content;
|
|
835
|
+
if (!Array.isArray(content))
|
|
836
|
+
return;
|
|
837
|
+
for (const block of content) {
|
|
838
|
+
if (block.type === 'tool_use') {
|
|
839
|
+
const name = block.name;
|
|
840
|
+
const input = block.input;
|
|
841
|
+
const toolUseId = block.id;
|
|
842
|
+
if (!input)
|
|
843
|
+
continue;
|
|
844
|
+
if (name === 'TaskCreate' && toolUseId) {
|
|
845
|
+
this.pendingTaskCreates.set(toolUseId, {
|
|
846
|
+
subject: input.subject || 'Untitled',
|
|
847
|
+
description: input.description,
|
|
848
|
+
activeForm: input.activeForm,
|
|
849
|
+
subagentType: input.subagentType,
|
|
850
|
+
isGoalGate: input.isGoalGate,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
else if (name === 'TaskUpdate') {
|
|
854
|
+
this.applyTaskUpdate(input);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
else if (block.type === 'tool_result') {
|
|
858
|
+
const toolUseId = block.tool_use_id;
|
|
859
|
+
if (!toolUseId)
|
|
860
|
+
continue;
|
|
861
|
+
this.resolveTaskCreate(toolUseId, block.content ?? block.output, event.timestamp);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
866
|
+
// Private: Task State from FollowEvent
|
|
867
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
868
|
+
extractTaskStateFromFollowEvent(event) {
|
|
869
|
+
const raw = event.raw;
|
|
870
|
+
if (event.type === 'tool_use' && event.toolName) {
|
|
871
|
+
if (!raw?.input)
|
|
872
|
+
return;
|
|
873
|
+
const input = raw.input;
|
|
874
|
+
const toolUseId = raw.id;
|
|
875
|
+
if (event.toolName === 'TaskCreate' && toolUseId) {
|
|
876
|
+
this.pendingTaskCreates.set(toolUseId, {
|
|
877
|
+
subject: input.subject || 'Untitled',
|
|
878
|
+
description: input.description,
|
|
879
|
+
activeForm: input.activeForm,
|
|
880
|
+
subagentType: input.subagentType,
|
|
881
|
+
isGoalGate: input.isGoalGate,
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
else if (event.toolName === 'TaskUpdate') {
|
|
885
|
+
this.applyTaskUpdate(input);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
else if (event.type === 'tool_result') {
|
|
889
|
+
if (!raw)
|
|
890
|
+
return;
|
|
891
|
+
const toolUseId = raw.tool_use_id;
|
|
892
|
+
if (!toolUseId)
|
|
893
|
+
return;
|
|
894
|
+
this.resolveTaskCreate(toolUseId, raw.content, event.timestamp);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
applyTaskUpdate(input) {
|
|
898
|
+
const taskId = input.taskId;
|
|
899
|
+
if (!taskId)
|
|
900
|
+
return;
|
|
901
|
+
const existing = this.tasks.get(taskId);
|
|
902
|
+
if (existing) {
|
|
903
|
+
if (input.status) {
|
|
904
|
+
const newStatus = input.status;
|
|
905
|
+
if (newStatus === 'deleted') {
|
|
906
|
+
this.tasks.delete(taskId);
|
|
907
|
+
if (this.activeTaskId === taskId)
|
|
908
|
+
this.activeTaskId = null;
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
existing.status = newStatus;
|
|
912
|
+
existing.updatedAt = new Date();
|
|
913
|
+
if (newStatus === 'in_progress') {
|
|
914
|
+
this.activeTaskId = taskId;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
if (input.subject)
|
|
918
|
+
existing.subject = input.subject;
|
|
919
|
+
if (input.description)
|
|
920
|
+
existing.description = input.description;
|
|
921
|
+
if (input.activeForm)
|
|
922
|
+
existing.activeForm = input.activeForm;
|
|
923
|
+
if (input.addBlockedBy && Array.isArray(input.addBlockedBy)) {
|
|
924
|
+
existing.blockedBy.push(...input.addBlockedBy);
|
|
925
|
+
}
|
|
926
|
+
if (input.addBlocks && Array.isArray(input.addBlocks)) {
|
|
927
|
+
existing.blocks.push(...input.addBlocks);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
// TaskUpdate for unknown task — create placeholder
|
|
932
|
+
const status = input.status || 'pending';
|
|
933
|
+
if (status === 'deleted')
|
|
934
|
+
return;
|
|
935
|
+
const now = new Date();
|
|
936
|
+
this.tasks.set(taskId, {
|
|
937
|
+
taskId,
|
|
938
|
+
subject: input.subject || `Task ${taskId}`,
|
|
939
|
+
status: status,
|
|
940
|
+
createdAt: now,
|
|
941
|
+
updatedAt: now,
|
|
942
|
+
blockedBy: [],
|
|
943
|
+
blocks: [],
|
|
944
|
+
associatedToolCalls: [],
|
|
945
|
+
activeForm: input.activeForm,
|
|
946
|
+
});
|
|
947
|
+
if (status === 'in_progress') {
|
|
948
|
+
this.activeTaskId = taskId;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
resolveTaskCreate(toolUseId, content, timestamp) {
|
|
953
|
+
const pending = this.pendingTaskCreates.get(toolUseId);
|
|
954
|
+
if (!pending)
|
|
955
|
+
return;
|
|
956
|
+
this.pendingTaskCreates.delete(toolUseId);
|
|
957
|
+
const taskId = this.extractTaskIdFromResult(content);
|
|
958
|
+
if (!taskId)
|
|
959
|
+
return;
|
|
960
|
+
const now = new Date(timestamp);
|
|
961
|
+
this.tasks.set(taskId, {
|
|
962
|
+
taskId,
|
|
963
|
+
subject: pending.subject,
|
|
964
|
+
description: pending.description,
|
|
965
|
+
status: 'pending',
|
|
966
|
+
createdAt: now,
|
|
967
|
+
updatedAt: now,
|
|
968
|
+
blockedBy: [],
|
|
969
|
+
blocks: [],
|
|
970
|
+
associatedToolCalls: [],
|
|
971
|
+
activeForm: pending.activeForm,
|
|
972
|
+
subagentType: pending.subagentType,
|
|
973
|
+
isGoalGate: pending.isGoalGate,
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
977
|
+
// Private: Subagent Tracking from SessionEvent
|
|
978
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
979
|
+
extractSubagentFromEvent(event) {
|
|
980
|
+
const content = event.message.content;
|
|
981
|
+
if (!Array.isArray(content))
|
|
982
|
+
return;
|
|
983
|
+
for (const block of content) {
|
|
984
|
+
if (block.type === 'tool_use' && block.name === 'Task') {
|
|
985
|
+
const input = block.input;
|
|
986
|
+
const toolUseId = block.id;
|
|
987
|
+
if (!input || !toolUseId)
|
|
988
|
+
continue;
|
|
989
|
+
const info = {
|
|
990
|
+
id: toolUseId,
|
|
991
|
+
description: input.description || 'Unknown task',
|
|
992
|
+
subagentType: input.subagent_type || input.subagentType || 'general',
|
|
993
|
+
spawnTime: event.timestamp,
|
|
994
|
+
status: 'running',
|
|
995
|
+
};
|
|
996
|
+
const idx = this.subagents.length;
|
|
997
|
+
this.subagents.push(info);
|
|
998
|
+
this.pendingSubagents.set(toolUseId, idx);
|
|
999
|
+
}
|
|
1000
|
+
else if (block.type === 'tool_result') {
|
|
1001
|
+
const toolUseId = block.tool_use_id;
|
|
1002
|
+
if (!toolUseId)
|
|
1003
|
+
continue;
|
|
1004
|
+
this.completeSubagent(toolUseId, event.timestamp);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1009
|
+
// Private: Subagent Tracking from FollowEvent
|
|
1010
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1011
|
+
extractSubagentFromFollowEvent(event) {
|
|
1012
|
+
const raw = event.raw;
|
|
1013
|
+
if (event.type === 'tool_use' && event.toolName === 'Task') {
|
|
1014
|
+
if (!raw?.input)
|
|
1015
|
+
return;
|
|
1016
|
+
const input = raw.input;
|
|
1017
|
+
const toolUseId = raw.id || '';
|
|
1018
|
+
const info = {
|
|
1019
|
+
id: toolUseId,
|
|
1020
|
+
description: input.description || 'Unknown task',
|
|
1021
|
+
subagentType: input.subagent_type || input.subagentType || 'general',
|
|
1022
|
+
spawnTime: event.timestamp,
|
|
1023
|
+
status: 'running',
|
|
1024
|
+
};
|
|
1025
|
+
const idx = this.subagents.length;
|
|
1026
|
+
this.subagents.push(info);
|
|
1027
|
+
if (toolUseId) {
|
|
1028
|
+
this.pendingSubagents.set(toolUseId, idx);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else if (event.type === 'tool_result') {
|
|
1032
|
+
if (!raw)
|
|
1033
|
+
return;
|
|
1034
|
+
const toolUseId = raw.tool_use_id;
|
|
1035
|
+
if (!toolUseId)
|
|
1036
|
+
return;
|
|
1037
|
+
this.completeSubagent(toolUseId, event.timestamp);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
completeSubagent(toolUseId, timestamp) {
|
|
1041
|
+
const idx = this.pendingSubagents.get(toolUseId);
|
|
1042
|
+
if (idx === undefined)
|
|
1043
|
+
return;
|
|
1044
|
+
this.pendingSubagents.delete(toolUseId);
|
|
1045
|
+
const agent = this.subagents[idx];
|
|
1046
|
+
agent.status = 'completed';
|
|
1047
|
+
agent.completionTime = timestamp;
|
|
1048
|
+
const start = new Date(agent.spawnTime).getTime();
|
|
1049
|
+
const end = new Date(timestamp).getTime();
|
|
1050
|
+
if (!isNaN(start) && !isNaN(end) && end >= start) {
|
|
1051
|
+
agent.durationMs = end - start;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1055
|
+
// Private: Plan Extraction from SessionEvent
|
|
1056
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1057
|
+
extractPlanFromSessionEvent(event) {
|
|
1058
|
+
// Use toFollowEvents to properly split assistant messages into
|
|
1059
|
+
// per-content-block FollowEvents (one per tool_use + one for text).
|
|
1060
|
+
// This ensures PlanExtractor sees type:'tool_use' for plan tools
|
|
1061
|
+
// like EnterPlanMode, with raw.input set correctly.
|
|
1062
|
+
const providerId = this._providerId ?? 'claude-code';
|
|
1063
|
+
const followEvents = (0, eventBridge_1.toFollowEvents)(event, providerId);
|
|
1064
|
+
for (const fe of followEvents) {
|
|
1065
|
+
this.planExtractor.processEvent(fe);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
convertExtractedPlanToPlanState(extracted) {
|
|
1069
|
+
const completed = extracted.steps.filter(s => s.status === 'completed').length;
|
|
1070
|
+
const total = extracted.steps.length;
|
|
1071
|
+
return {
|
|
1072
|
+
active: true,
|
|
1073
|
+
steps: extracted.steps.map(s => ({
|
|
1074
|
+
id: s.id,
|
|
1075
|
+
description: s.description,
|
|
1076
|
+
status: s.status,
|
|
1077
|
+
phase: s.phase,
|
|
1078
|
+
complexity: s.complexity,
|
|
1079
|
+
})),
|
|
1080
|
+
title: extracted.title,
|
|
1081
|
+
source: extracted.source,
|
|
1082
|
+
completionRate: total > 0 ? completed / total : 0,
|
|
1083
|
+
rawMarkdown: extracted.rawMarkdown,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1087
|
+
// Private: Context Attribution from SessionEvent
|
|
1088
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1089
|
+
attributeContextFromEvent(event) {
|
|
1090
|
+
const content = event.message.content;
|
|
1091
|
+
if (event.type === 'user') {
|
|
1092
|
+
if (Array.isArray(content)) {
|
|
1093
|
+
for (const block of content) {
|
|
1094
|
+
if (block.type === 'tool_result') {
|
|
1095
|
+
const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || '');
|
|
1096
|
+
this.contextAttribution.toolOutputs += this.estimateTokens(text);
|
|
1097
|
+
}
|
|
1098
|
+
else if (block.type === 'text') {
|
|
1099
|
+
const text = block.text || '';
|
|
1100
|
+
if (this.isSystemPromptContent(text)) {
|
|
1101
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(text);
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
this.contextAttribution.userMessages += this.estimateTokens(text);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
else if (typeof content === 'string') {
|
|
1110
|
+
if (this.isSystemPromptContent(content)) {
|
|
1111
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(content);
|
|
1112
|
+
}
|
|
1113
|
+
else {
|
|
1114
|
+
this.contextAttribution.userMessages += this.estimateTokens(content);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
else if (event.type === 'assistant') {
|
|
1119
|
+
if (Array.isArray(content)) {
|
|
1120
|
+
for (const block of content) {
|
|
1121
|
+
if (block.type === 'thinking') {
|
|
1122
|
+
this.contextAttribution.thinking += this.estimateTokens(block.thinking || '');
|
|
1123
|
+
}
|
|
1124
|
+
else if (block.type === 'tool_use') {
|
|
1125
|
+
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input || '');
|
|
1126
|
+
this.contextAttribution.toolInputs += this.estimateTokens(input);
|
|
1127
|
+
}
|
|
1128
|
+
else if (block.type === 'text') {
|
|
1129
|
+
this.contextAttribution.assistantResponses += this.estimateTokens(block.text || '');
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
else if (event.type === 'summary') {
|
|
1135
|
+
const text = this.extractTextContent(event) || '';
|
|
1136
|
+
if (text) {
|
|
1137
|
+
this.contextAttribution.other += this.estimateTokens(text);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1142
|
+
// Private: Context Attribution from FollowEvent
|
|
1143
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1144
|
+
attributeContextFromFollowEvent(event) {
|
|
1145
|
+
const raw = event.raw;
|
|
1146
|
+
// Try to get full content blocks from raw.message.content
|
|
1147
|
+
const message = raw?.message;
|
|
1148
|
+
const content = message?.content;
|
|
1149
|
+
if (event.type === 'user') {
|
|
1150
|
+
if (Array.isArray(content)) {
|
|
1151
|
+
for (const block of content) {
|
|
1152
|
+
if (block.type === 'tool_result') {
|
|
1153
|
+
const text = typeof block.content === 'string' ? block.content : JSON.stringify(block.content || '');
|
|
1154
|
+
this.contextAttribution.toolOutputs += this.estimateTokens(text);
|
|
1155
|
+
}
|
|
1156
|
+
else if (block.type === 'text') {
|
|
1157
|
+
const text = block.text || '';
|
|
1158
|
+
if (this.isSystemPromptContent(text)) {
|
|
1159
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(text);
|
|
1160
|
+
}
|
|
1161
|
+
else {
|
|
1162
|
+
this.contextAttribution.userMessages += this.estimateTokens(text);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
else if (event.summary) {
|
|
1168
|
+
if (this.isSystemPromptContent(event.summary)) {
|
|
1169
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(event.summary);
|
|
1170
|
+
}
|
|
1171
|
+
else {
|
|
1172
|
+
this.contextAttribution.userMessages += this.estimateTokens(event.summary);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
else if (event.type === 'assistant') {
|
|
1177
|
+
if (Array.isArray(content)) {
|
|
1178
|
+
for (const block of content) {
|
|
1179
|
+
if (block.type === 'thinking') {
|
|
1180
|
+
this.contextAttribution.thinking += this.estimateTokens(block.thinking || '');
|
|
1181
|
+
}
|
|
1182
|
+
else if (block.type === 'tool_use') {
|
|
1183
|
+
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input || '');
|
|
1184
|
+
this.contextAttribution.toolInputs += this.estimateTokens(input);
|
|
1185
|
+
}
|
|
1186
|
+
else if (block.type === 'text') {
|
|
1187
|
+
this.contextAttribution.assistantResponses += this.estimateTokens(block.text || '');
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
else if (event.summary) {
|
|
1192
|
+
this.contextAttribution.assistantResponses += this.estimateTokens(event.summary);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
else if (event.type === 'tool_use') {
|
|
1196
|
+
// Try raw input first (summary is truncated to ~80 chars)
|
|
1197
|
+
const rawInput = raw?.input != null ? JSON.stringify(raw.input) : null;
|
|
1198
|
+
const text = rawInput || event.summary || '';
|
|
1199
|
+
if (text)
|
|
1200
|
+
this.contextAttribution.toolInputs += this.estimateTokens(text);
|
|
1201
|
+
}
|
|
1202
|
+
else if (event.type === 'tool_result') {
|
|
1203
|
+
const rawContent = raw?.content;
|
|
1204
|
+
const text = typeof rawContent === 'string' ? rawContent
|
|
1205
|
+
: rawContent ? JSON.stringify(rawContent)
|
|
1206
|
+
: event.summary || '';
|
|
1207
|
+
if (text)
|
|
1208
|
+
this.contextAttribution.toolOutputs += this.estimateTokens(text);
|
|
1209
|
+
}
|
|
1210
|
+
else if (event.type === 'summary') {
|
|
1211
|
+
if (event.summary) {
|
|
1212
|
+
this.contextAttribution.other += this.estimateTokens(event.summary);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1217
|
+
// Private: Permission Mode
|
|
1218
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1219
|
+
trackPermissionMode(event) {
|
|
1220
|
+
if (!event.permissionMode)
|
|
1221
|
+
return;
|
|
1222
|
+
const mode = event.permissionMode;
|
|
1223
|
+
if (mode !== this.currentPermissionMode) {
|
|
1224
|
+
this.permissionModeHistory.push({
|
|
1225
|
+
timestamp: event.timestamp,
|
|
1226
|
+
mode,
|
|
1227
|
+
previousMode: this.currentPermissionMode,
|
|
1228
|
+
});
|
|
1229
|
+
this.currentPermissionMode = mode;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1233
|
+
// Private: Timeline
|
|
1234
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1235
|
+
addTimelineFromSessionEvent(event) {
|
|
1236
|
+
// Hard noise: skip entirely
|
|
1237
|
+
if ((0, noiseClassifier_1.isHardNoise)(event))
|
|
1238
|
+
return;
|
|
1239
|
+
let tlType;
|
|
1240
|
+
let description;
|
|
1241
|
+
let noiseLevel = 'system';
|
|
1242
|
+
const metadata = {};
|
|
1243
|
+
switch (event.type) {
|
|
1244
|
+
case 'user':
|
|
1245
|
+
tlType = 'user_prompt';
|
|
1246
|
+
description = this.extractTextContent(event) ?? 'User prompt';
|
|
1247
|
+
noiseLevel = 'user';
|
|
1248
|
+
break;
|
|
1249
|
+
case 'assistant':
|
|
1250
|
+
tlType = 'assistant_response';
|
|
1251
|
+
description = this.extractTextContent(event) ?? 'Assistant response';
|
|
1252
|
+
noiseLevel = 'ai';
|
|
1253
|
+
if (event.message.model)
|
|
1254
|
+
metadata.model = event.message.model;
|
|
1255
|
+
if (event.message.usage) {
|
|
1256
|
+
metadata.tokenCount = event.message.usage.input_tokens + event.message.usage.output_tokens;
|
|
1257
|
+
}
|
|
1258
|
+
break;
|
|
1259
|
+
case 'tool_use': {
|
|
1260
|
+
tlType = 'tool_call';
|
|
1261
|
+
// Use rich tool summary formatter
|
|
1262
|
+
const toolName = event.tool?.name ?? 'unknown';
|
|
1263
|
+
const toolSummary = event.tool?.input
|
|
1264
|
+
? (0, toolSummary_1.formatToolSummary)(toolName, event.tool.input)
|
|
1265
|
+
: '';
|
|
1266
|
+
description = toolSummary ? `${toolName}: ${toolSummary}` : toolName;
|
|
1267
|
+
noiseLevel = 'system';
|
|
1268
|
+
if (event.tool)
|
|
1269
|
+
metadata.toolName = event.tool.name;
|
|
1270
|
+
break;
|
|
1271
|
+
}
|
|
1272
|
+
case 'tool_result':
|
|
1273
|
+
tlType = 'tool_result';
|
|
1274
|
+
description = event.result ? `Result for tool call` : 'Tool result';
|
|
1275
|
+
noiseLevel = 'noise';
|
|
1276
|
+
if (event.result?.is_error)
|
|
1277
|
+
metadata.isError = true;
|
|
1278
|
+
break;
|
|
1279
|
+
case 'summary':
|
|
1280
|
+
tlType = 'compaction';
|
|
1281
|
+
description = 'Context compacted';
|
|
1282
|
+
noiseLevel = 'system';
|
|
1283
|
+
break;
|
|
1284
|
+
default:
|
|
1285
|
+
tlType = 'session_start';
|
|
1286
|
+
description = 'Event';
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
// Truncate description for timeline
|
|
1290
|
+
if (description.length > 200) {
|
|
1291
|
+
description = description.substring(0, 197) + '...';
|
|
1292
|
+
}
|
|
1293
|
+
// Classify noise
|
|
1294
|
+
const softNoiseReason = (0, noiseClassifier_1.getSoftNoiseReason)(event);
|
|
1295
|
+
const messageClassification = (0, noiseClassifier_1.classifyMessage)(event);
|
|
1296
|
+
this.timeline.push({
|
|
1297
|
+
type: tlType,
|
|
1298
|
+
timestamp: event.timestamp,
|
|
1299
|
+
description,
|
|
1300
|
+
noiseLevel,
|
|
1301
|
+
softNoiseReason: softNoiseReason ?? undefined,
|
|
1302
|
+
messageClassification,
|
|
1303
|
+
isSidechain: event.isSidechain,
|
|
1304
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1305
|
+
});
|
|
1306
|
+
// Cap timeline
|
|
1307
|
+
if (this.timeline.length > this.timelineCap) {
|
|
1308
|
+
this.timeline.shift();
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
addTimelineFromFollowEvent(event) {
|
|
1312
|
+
// Hard noise: skip entirely
|
|
1313
|
+
if ((0, noiseClassifier_1.isHardNoiseFollowEvent)(event))
|
|
1314
|
+
return;
|
|
1315
|
+
let tlType;
|
|
1316
|
+
let description = event.summary || '';
|
|
1317
|
+
let noiseLevel = 'system';
|
|
1318
|
+
const metadata = {};
|
|
1319
|
+
switch (event.type) {
|
|
1320
|
+
case 'user':
|
|
1321
|
+
tlType = 'user_prompt';
|
|
1322
|
+
noiseLevel = 'user';
|
|
1323
|
+
break;
|
|
1324
|
+
case 'assistant':
|
|
1325
|
+
tlType = 'assistant_response';
|
|
1326
|
+
noiseLevel = 'ai';
|
|
1327
|
+
if (event.model)
|
|
1328
|
+
metadata.model = event.model;
|
|
1329
|
+
if (event.tokens)
|
|
1330
|
+
metadata.tokenCount = event.tokens.input + event.tokens.output;
|
|
1331
|
+
break;
|
|
1332
|
+
case 'tool_use':
|
|
1333
|
+
tlType = 'tool_call';
|
|
1334
|
+
noiseLevel = 'system';
|
|
1335
|
+
if (event.toolName)
|
|
1336
|
+
metadata.toolName = event.toolName;
|
|
1337
|
+
break;
|
|
1338
|
+
case 'tool_result':
|
|
1339
|
+
tlType = 'tool_result';
|
|
1340
|
+
noiseLevel = 'noise';
|
|
1341
|
+
break;
|
|
1342
|
+
case 'summary':
|
|
1343
|
+
tlType = 'compaction';
|
|
1344
|
+
noiseLevel = 'system';
|
|
1345
|
+
break;
|
|
1346
|
+
default:
|
|
1347
|
+
tlType = 'session_start';
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
// Truncate description for timeline
|
|
1351
|
+
if (description.length > 200) {
|
|
1352
|
+
description = description.substring(0, 197) + '...';
|
|
1353
|
+
}
|
|
1354
|
+
// Classify message
|
|
1355
|
+
const messageClassification = (0, noiseClassifier_1.classifyFollowEvent)(event);
|
|
1356
|
+
this.timeline.push({
|
|
1357
|
+
type: tlType,
|
|
1358
|
+
timestamp: event.timestamp,
|
|
1359
|
+
description,
|
|
1360
|
+
noiseLevel,
|
|
1361
|
+
messageClassification,
|
|
1362
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1363
|
+
});
|
|
1364
|
+
// Cap timeline
|
|
1365
|
+
if (this.timeline.length > this.timelineCap) {
|
|
1366
|
+
this.timeline.shift();
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1370
|
+
// Private: Helper methods
|
|
1371
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1372
|
+
estimateTokens(text) {
|
|
1373
|
+
return Math.ceil(text.length / 4);
|
|
1374
|
+
}
|
|
1375
|
+
hasTextContent(event) {
|
|
1376
|
+
const content = event.message.content;
|
|
1377
|
+
if (typeof content === 'string')
|
|
1378
|
+
return content.length > 0;
|
|
1379
|
+
if (Array.isArray(content)) {
|
|
1380
|
+
return content.some(b => b.type === 'text' && typeof b.text === 'string' && b.text.length > 0);
|
|
1381
|
+
}
|
|
1382
|
+
return false;
|
|
1383
|
+
}
|
|
1384
|
+
extractTextContent(event) {
|
|
1385
|
+
const content = event.message.content;
|
|
1386
|
+
if (typeof content === 'string')
|
|
1387
|
+
return content;
|
|
1388
|
+
if (Array.isArray(content)) {
|
|
1389
|
+
for (const block of content) {
|
|
1390
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
1391
|
+
return block.text;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
extractTaskIdFromResult(content) {
|
|
1398
|
+
const str = typeof content === 'string' ? content : JSON.stringify(content || '');
|
|
1399
|
+
const taskMatch = str.match(/Task #(\d+)/i);
|
|
1400
|
+
if (taskMatch)
|
|
1401
|
+
return taskMatch[1];
|
|
1402
|
+
const jsonMatch = str.match(/"taskId"\s*:\s*"?(\d+)"?/i);
|
|
1403
|
+
if (jsonMatch)
|
|
1404
|
+
return jsonMatch[1];
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
isSystemPromptContent(text) {
|
|
1408
|
+
return text.includes('<system-reminder>') || text.includes('CLAUDE.md');
|
|
1409
|
+
}
|
|
1410
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1411
|
+
// Private: Gonzo/Lazyjournal analytics
|
|
1412
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1413
|
+
extractToolNameFromEvent(event) {
|
|
1414
|
+
const content = event.message.content;
|
|
1415
|
+
if (!Array.isArray(content))
|
|
1416
|
+
return undefined;
|
|
1417
|
+
for (const block of content) {
|
|
1418
|
+
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
1419
|
+
return block.name;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
/** Feed event data into frequency trackers, pattern extractor, and heatmap. */
|
|
1425
|
+
feedAnalytics(timestamp, summary, toolName) {
|
|
1426
|
+
// Heatmap: record every event
|
|
1427
|
+
this.heatmapTracker.record(timestamp);
|
|
1428
|
+
// Tool frequency
|
|
1429
|
+
if (toolName) {
|
|
1430
|
+
this.toolFrequency.increment(toolName, timestamp);
|
|
1431
|
+
}
|
|
1432
|
+
// Word frequency: extract significant words from summary
|
|
1433
|
+
if (summary) {
|
|
1434
|
+
const words = summary.split(/\s+/).filter(w => w.length > 2);
|
|
1435
|
+
for (const word of words) {
|
|
1436
|
+
this.wordFrequency.increment(word.toLowerCase(), timestamp);
|
|
1437
|
+
}
|
|
1438
|
+
// Pattern extraction
|
|
1439
|
+
this.patternExtractor.add(summary);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
exports.EventAggregator = EventAggregator;
|