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.
Files changed (126) hide show
  1. package/README.md +92 -0
  2. package/dist/aggregation/EventAggregator.d.ts +172 -0
  3. package/dist/aggregation/EventAggregator.js +1443 -0
  4. package/dist/aggregation/FrequencyTracker.d.ts +42 -0
  5. package/dist/aggregation/FrequencyTracker.js +73 -0
  6. package/dist/aggregation/HeatmapTracker.d.ts +40 -0
  7. package/dist/aggregation/HeatmapTracker.js +93 -0
  8. package/dist/aggregation/PatternExtractor.d.ts +51 -0
  9. package/dist/aggregation/PatternExtractor.js +171 -0
  10. package/dist/aggregation/snapshot.d.ts +64 -0
  11. package/dist/aggregation/snapshot.js +151 -0
  12. package/dist/aggregation/types.d.ts +121 -0
  13. package/dist/aggregation/types.js +6 -0
  14. package/dist/context/composer.d.ts +31 -0
  15. package/dist/context/composer.js +72 -0
  16. package/dist/credentials.d.ts +23 -0
  17. package/dist/credentials.js +96 -0
  18. package/dist/formatters/eventHighlighter.d.ts +30 -0
  19. package/dist/formatters/eventHighlighter.js +217 -0
  20. package/dist/formatters/noiseClassifier.d.ts +73 -0
  21. package/dist/formatters/noiseClassifier.js +226 -0
  22. package/dist/formatters/sessionDump.d.ts +38 -0
  23. package/dist/formatters/sessionDump.js +313 -0
  24. package/dist/formatters/toolSummary.d.ts +23 -0
  25. package/dist/formatters/toolSummary.js +230 -0
  26. package/dist/index.d.ts +85 -0
  27. package/dist/index.js +182 -0
  28. package/dist/parsers/changelogParser.d.ts +25 -0
  29. package/dist/parsers/changelogParser.js +74 -0
  30. package/dist/parsers/codexParser.d.ts +76 -0
  31. package/dist/parsers/codexParser.js +653 -0
  32. package/dist/parsers/debugLogParser.d.ts +63 -0
  33. package/dist/parsers/debugLogParser.js +164 -0
  34. package/dist/parsers/jsonl.d.ts +45 -0
  35. package/dist/parsers/jsonl.js +57 -0
  36. package/dist/parsers/openCodeParser.d.ts +64 -0
  37. package/dist/parsers/openCodeParser.js +581 -0
  38. package/dist/parsers/planExtractor.d.ts +63 -0
  39. package/dist/parsers/planExtractor.js +330 -0
  40. package/dist/parsers/sessionActivityDetector.d.ts +31 -0
  41. package/dist/parsers/sessionActivityDetector.js +184 -0
  42. package/dist/parsers/sessionPathResolver.d.ts +230 -0
  43. package/dist/parsers/sessionPathResolver.js +753 -0
  44. package/dist/parsers/subagentScanner.d.ts +43 -0
  45. package/dist/parsers/subagentScanner.js +366 -0
  46. package/dist/parsers/subagentTraceParser.d.ts +58 -0
  47. package/dist/parsers/subagentTraceParser.js +346 -0
  48. package/dist/paths.d.ts +38 -0
  49. package/dist/paths.js +107 -0
  50. package/dist/phrases.d.ts +52 -0
  51. package/dist/phrases.js +1333 -0
  52. package/dist/providers/claudeCode.d.ts +48 -0
  53. package/dist/providers/claudeCode.js +465 -0
  54. package/dist/providers/codex.d.ts +57 -0
  55. package/dist/providers/codex.js +944 -0
  56. package/dist/providers/codexDatabase.d.ts +37 -0
  57. package/dist/providers/codexDatabase.js +148 -0
  58. package/dist/providers/detect.d.ts +16 -0
  59. package/dist/providers/detect.js +162 -0
  60. package/dist/providers/openCode.d.ts +70 -0
  61. package/dist/providers/openCode.js +1524 -0
  62. package/dist/providers/openCodeDatabase.d.ts +87 -0
  63. package/dist/providers/openCodeDatabase.js +232 -0
  64. package/dist/providers/types.d.ts +154 -0
  65. package/dist/providers/types.js +12 -0
  66. package/dist/quota.d.ts +34 -0
  67. package/dist/quota.js +80 -0
  68. package/dist/readers/decisions.d.ts +10 -0
  69. package/dist/readers/decisions.js +27 -0
  70. package/dist/readers/handoff.d.ts +4 -0
  71. package/dist/readers/handoff.js +51 -0
  72. package/dist/readers/helpers.d.ts +7 -0
  73. package/dist/readers/helpers.js +52 -0
  74. package/dist/readers/history.d.ts +5 -0
  75. package/dist/readers/history.js +12 -0
  76. package/dist/readers/notes.d.ts +10 -0
  77. package/dist/readers/notes.js +46 -0
  78. package/dist/readers/plans.d.ts +35 -0
  79. package/dist/readers/plans.js +247 -0
  80. package/dist/readers/tasks.d.ts +8 -0
  81. package/dist/readers/tasks.js +22 -0
  82. package/dist/report/htmlHelpers.d.ts +18 -0
  83. package/dist/report/htmlHelpers.js +166 -0
  84. package/dist/report/htmlReportGenerator.d.ts +11 -0
  85. package/dist/report/htmlReportGenerator.js +650 -0
  86. package/dist/report/index.d.ts +8 -0
  87. package/dist/report/index.js +16 -0
  88. package/dist/report/logo.d.ts +2 -0
  89. package/dist/report/logo.js +5 -0
  90. package/dist/report/openBrowser.d.ts +5 -0
  91. package/dist/report/openBrowser.js +22 -0
  92. package/dist/report/transcriptParser.d.ts +12 -0
  93. package/dist/report/transcriptParser.js +177 -0
  94. package/dist/report/types.d.ts +43 -0
  95. package/dist/report/types.js +5 -0
  96. package/dist/search/advancedFilter.d.ts +62 -0
  97. package/dist/search/advancedFilter.js +201 -0
  98. package/dist/search/sessionSearch.d.ts +16 -0
  99. package/dist/search/sessionSearch.js +93 -0
  100. package/dist/types/codex.d.ts +276 -0
  101. package/dist/types/codex.js +14 -0
  102. package/dist/types/decisionLog.d.ts +23 -0
  103. package/dist/types/decisionLog.js +8 -0
  104. package/dist/types/historicalData.d.ts +74 -0
  105. package/dist/types/historicalData.js +17 -0
  106. package/dist/types/knowledgeNote.d.ts +40 -0
  107. package/dist/types/knowledgeNote.js +18 -0
  108. package/dist/types/opencode.d.ts +268 -0
  109. package/dist/types/opencode.js +13 -0
  110. package/dist/types/plan.d.ts +49 -0
  111. package/dist/types/plan.js +10 -0
  112. package/dist/types/sessionEvent.d.ts +562 -0
  113. package/dist/types/sessionEvent.js +11 -0
  114. package/dist/types/taskPersistence.d.ts +33 -0
  115. package/dist/types/taskPersistence.js +16 -0
  116. package/dist/watchers/eventBridge.d.ts +19 -0
  117. package/dist/watchers/eventBridge.js +162 -0
  118. package/dist/watchers/factory.d.ts +15 -0
  119. package/dist/watchers/factory.js +85 -0
  120. package/dist/watchers/jsonlWatcher.d.ts +30 -0
  121. package/dist/watchers/jsonlWatcher.js +444 -0
  122. package/dist/watchers/sqliteWatcher.d.ts +30 -0
  123. package/dist/watchers/sqliteWatcher.js +278 -0
  124. package/dist/watchers/types.d.ts +60 -0
  125. package/dist/watchers/types.js +5 -0
  126. 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;