sidekick-shared 0.27.6 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +58 -18
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -2
- package/dist/claudePaths.d.ts +11 -0
- package/dist/claudePaths.js +61 -0
- package/dist/claudeProfiles.d.ts +2 -9
- package/dist/claudeProfiles.js +10 -25
- package/dist/codexPaths.d.ts +22 -0
- package/dist/codexPaths.js +67 -0
- package/dist/codexProfiles.d.ts +1 -4
- package/dist/codexProfiles.js +5 -43
- package/dist/doctor.js +2 -2
- package/dist/esm/accountChangeSignal.js +15 -0
- package/dist/esm/accountChanges.js +152 -0
- package/dist/esm/accountHealth.js +288 -0
- package/dist/esm/accountLaunch.js +145 -0
- package/dist/esm/accountLease.js +60 -0
- package/dist/esm/accountManager.js +606 -0
- package/dist/esm/accountRegistry.js +214 -0
- package/dist/esm/accountStatus.js +51 -0
- package/dist/esm/accountSwitch.js +74 -0
- package/dist/esm/accountSync.js +156 -0
- package/dist/esm/accountSyncTypes.js +4 -0
- package/dist/esm/accounts.js +1291 -0
- package/dist/esm/aggregation/EventAggregator.js +2034 -0
- package/dist/esm/aggregation/FrequencyTracker.js +69 -0
- package/dist/esm/aggregation/HeatmapTracker.js +89 -0
- package/dist/esm/aggregation/PatternExtractor.js +168 -0
- package/dist/esm/aggregation/costProvenance.js +31 -0
- package/dist/esm/aggregation/snapshot.js +161 -0
- package/dist/esm/aggregation/types.js +5 -0
- package/dist/esm/analytics/codeImpact.js +18 -0
- package/dist/esm/analytics/compactionLedger.js +37 -0
- package/dist/esm/analytics/qualityScore.js +112 -0
- package/dist/esm/autoSwitch.js +185 -0
- package/dist/esm/browser.js +34 -0
- package/dist/esm/claudeCredentialDoctor.js +239 -0
- package/dist/esm/claudeCredentialLedger.js +91 -0
- package/dist/esm/claudeCredentials.js +111 -0
- package/dist/esm/claudeLiveGuard.js +43 -0
- package/dist/esm/claudePaths.js +22 -0
- package/dist/esm/claudeProfiles.js +142 -0
- package/dist/esm/claudeRefreshLock.js +106 -0
- package/dist/esm/codexAuth.js +101 -0
- package/dist/esm/codexCredentialDoctor.js +265 -0
- package/dist/esm/codexCredentialStore.js +164 -0
- package/dist/esm/codexKeepAlive.js +112 -0
- package/dist/esm/codexPaths.js +172 -0
- package/dist/esm/codexProfiles.js +1251 -0
- package/dist/esm/codexQuota.js +564 -0
- package/dist/esm/codexQuotaWatcher.js +363 -0
- package/dist/esm/context/composer.js +71 -0
- package/dist/esm/context/sessionContext.js +546 -0
- package/dist/esm/credentialIO.js +283 -0
- package/dist/esm/credentials.js +49 -0
- package/dist/esm/doctor.js +305 -0
- package/dist/esm/ensureDefaultAccounts.js +62 -0
- package/dist/esm/errorHistory.js +56 -0
- package/dist/esm/extractors/errorTaxonomy.js +43 -0
- package/dist/esm/extractors/gatherAssets.js +32 -0
- package/dist/esm/extractors/sessionAssets.js +176 -0
- package/dist/esm/extractors/sources/claudeAssets.js +158 -0
- package/dist/esm/extractors/sources/codexAssets.js +234 -0
- package/dist/esm/extractors/tokenUsage.js +41 -0
- package/dist/esm/extractors/toolCall.js +142 -0
- package/dist/esm/failingTools.js +65 -0
- package/dist/esm/fileHead.js +27 -0
- package/dist/esm/formatters/eventHighlighter.js +332 -0
- package/dist/esm/formatters/noiseClassifier.js +209 -0
- package/dist/esm/formatters/sessionDump.js +328 -0
- package/dist/esm/formatters/toolSummary.js +226 -0
- package/dist/esm/formatting.js +82 -0
- package/dist/esm/handoffUrl.js +23 -0
- package/dist/esm/historicalStore.js +305 -0
- package/dist/esm/humanPrompt.js +88 -0
- package/dist/esm/index.js +213 -0
- package/dist/esm/keepAlive.js +189 -0
- package/dist/esm/liveCredentialGuard.js +33 -0
- package/dist/esm/modelAliases.js +51 -0
- package/dist/esm/modelCatalog.js +139 -0
- package/dist/esm/modelContext.js +280 -0
- package/dist/esm/modelInfo.js +832 -0
- package/dist/esm/multiProviderQuotaService.js +450 -0
- package/dist/esm/node.js +46 -0
- package/dist/esm/observedContextWindows.js +165 -0
- package/dist/esm/observedSessionCollector.js +939 -0
- package/dist/esm/parsers/changelogParser.js +71 -0
- package/dist/esm/parsers/codexHistory.js +89 -0
- package/dist/esm/parsers/codexParser.js +906 -0
- package/dist/esm/parsers/debugLogParser.js +126 -0
- package/dist/esm/parsers/jsonl.js +66 -0
- package/dist/esm/parsers/mcpToolName.js +28 -0
- package/dist/esm/parsers/openCodeParser.js +625 -0
- package/dist/esm/parsers/planExtractor.js +335 -0
- package/dist/esm/parsers/sessionActivityDetector.js +246 -0
- package/dist/esm/parsers/sessionPathResolver.js +713 -0
- package/dist/esm/parsers/subagentScanner.js +360 -0
- package/dist/esm/parsers/subagentStatsCache.js +45 -0
- package/dist/esm/parsers/subagentTraceParser.js +325 -0
- package/dist/esm/paths.js +134 -0
- package/dist/esm/peakHours.js +194 -0
- package/dist/esm/phrases.js +1330 -0
- package/dist/esm/pricingCatalog.js +247 -0
- package/dist/esm/processDetection.js +200 -0
- package/dist/esm/projectMigration.js +59 -0
- package/dist/esm/promptHistory.js +1221 -0
- package/dist/esm/providerFailure.js +362 -0
- package/dist/esm/providerIds.js +6 -0
- package/dist/esm/providerQuota.js +1 -0
- package/dist/esm/providerServiceStatus.js +145 -0
- package/dist/esm/providerServiceStatusTypes.js +1 -0
- package/dist/esm/providerStatus.js +117 -0
- package/dist/esm/providers/claudeCode.js +575 -0
- package/dist/esm/providers/claudeListing.js +121 -0
- package/dist/esm/providers/codex.js +1034 -0
- package/dist/esm/providers/codexDatabase.js +297 -0
- package/dist/esm/providers/codexRollouts.js +215 -0
- package/dist/esm/providers/detect.js +185 -0
- package/dist/esm/providers/diagnostics.js +83 -0
- package/dist/esm/providers/directoryListingCache.js +206 -0
- package/dist/esm/providers/factory.js +54 -0
- package/dist/esm/providers/openCode.js +2008 -0
- package/dist/esm/providers/openCodeDatabase.js +419 -0
- package/dist/esm/providers/rolloutWalker.js +252 -0
- package/dist/esm/providers/types.js +12 -0
- package/dist/esm/quota.js +182 -0
- package/dist/esm/quotaHistory.js +364 -0
- package/dist/esm/quotaPoller.js +230 -0
- package/dist/esm/quotaPresentation.js +97 -0
- package/dist/esm/quotaResolver.js +231 -0
- package/dist/esm/quotaSnapshots.js +130 -0
- package/dist/esm/quotaThresholds.js +85 -0
- package/dist/esm/readers/decisions.js +24 -0
- package/dist/esm/readers/handoff.js +20 -0
- package/dist/esm/readers/helpers.js +27 -0
- package/dist/esm/readers/history.js +9 -0
- package/dist/esm/readers/notes.js +48 -0
- package/dist/esm/readers/plans.js +225 -0
- package/dist/esm/readers/tasks.js +19 -0
- package/dist/esm/report/htmlHelpers.js +280 -0
- package/dist/esm/report/htmlReportGenerator.js +704 -0
- package/dist/esm/report/index.js +7 -0
- package/dist/esm/report/logo.js +2 -0
- package/dist/esm/report/openBrowser.js +19 -0
- package/dist/esm/report/sessionReportInputs.js +33 -0
- package/dist/esm/report/transcriptParser.js +83 -0
- package/dist/esm/report/types.js +4 -0
- package/dist/esm/schemas/accountManager.js +204 -0
- package/dist/esm/schemas/accountStatus.js +25 -0
- package/dist/esm/schemas/assistantTurn.js +74 -0
- package/dist/esm/schemas/index.js +25 -0
- package/dist/esm/schemas/observedSessionV1.js +77 -0
- package/dist/esm/schemas/providerFailure.js +37 -0
- package/dist/esm/schemas/quota.js +132 -0
- package/dist/esm/schemas/quotaHistory.js +40 -0
- package/dist/esm/schemas/sessionEvent.js +216 -0
- package/dist/esm/schemas/stateFile.js +75 -0
- package/dist/esm/schemas/tokenEstimation.js +7 -0
- package/dist/esm/schemas/transcript.js +86 -0
- package/dist/esm/schemas/usageNormalization.js +38 -0
- package/dist/esm/search/advancedFilter.js +197 -0
- package/dist/esm/search/sessionSearch.js +66 -0
- package/dist/esm/sessionFingerprint.js +40 -0
- package/dist/esm/sessionMonitor.js +198 -0
- package/dist/esm/sessionPreviews.js +522 -0
- package/dist/esm/sessionPromptHistory.js +254 -0
- package/dist/esm/sessionPromptSignals.js +364 -0
- package/dist/esm/sessionStats.js +160 -0
- package/dist/esm/sessionTokenTotals.js +98 -0
- package/dist/esm/sessionTranscripts.js +49 -0
- package/dist/esm/stateFile.js +119 -0
- package/dist/esm/statusline/BurnRateCalculator.js +40 -0
- package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
- package/dist/esm/statusline/formatter.js +122 -0
- package/dist/esm/statusline/index.js +8 -0
- package/dist/esm/terminalSync.js +95 -0
- package/dist/esm/testing/credentialGuard.js +197 -0
- package/dist/esm/testing/index.js +12 -0
- package/dist/esm/testing/sessionLogFixtures.js +128 -0
- package/dist/esm/tokenEstimation.js +67 -0
- package/dist/esm/tokenSummary.js +113 -0
- package/dist/esm/toolCategory.js +82 -0
- package/dist/esm/transcript.js +257 -0
- package/dist/esm/turns/assistantTurn.js +373 -0
- package/dist/esm/types/codex.js +23 -0
- package/dist/esm/types/decisionLog.js +5 -0
- package/dist/esm/types/historicalData.js +47 -0
- package/dist/esm/types/knowledgeNote.js +15 -0
- package/dist/esm/types/observedSessionV1.js +257 -0
- package/dist/esm/types/opencode.js +12 -0
- package/dist/esm/types/plan.js +7 -0
- package/dist/esm/types/sessionEvent.js +10 -0
- package/dist/esm/types/taskPersistence.js +12 -0
- package/dist/esm/usage/billingBlocks.js +148 -0
- package/dist/esm/usage/claudeUsageDedupe.js +189 -0
- package/dist/esm/usage/importSessionHistory.js +84 -0
- package/dist/esm/usage/subagentUsage.js +18 -0
- package/dist/esm/usage/usageEvents.js +402 -0
- package/dist/esm/usage/usageReports.js +202 -0
- package/dist/esm/usageNormalization.js +95 -0
- package/dist/esm/watchers/eventBridge.js +219 -0
- package/dist/esm/watchers/factory.js +63 -0
- package/dist/esm/watchers/jsonlTail.js +289 -0
- package/dist/esm/watchers/jsonlWatcher.js +435 -0
- package/dist/esm/watchers/providerReaderWatcher.js +118 -0
- package/dist/esm/watchers/sqliteWatcher.js +285 -0
- package/dist/esm/watchers/types.js +4 -0
- package/dist/esm/workspaceRoots.js +56 -0
- package/dist/esm/writers/atomic.js +375 -0
- package/dist/esm/writers/decisions.js +51 -0
- package/dist/esm/writers/notes.js +46 -0
- package/dist/esm/writers/tasks.js +63 -0
- package/dist/esm/zaiQuota.js +323 -0
- package/dist/esm/zaiQuotaApi.js +271 -0
- package/dist/esm/zaiQuotaWatcher.js +265 -0
- package/dist/fileHead.d.ts +22 -0
- package/dist/fileHead.js +33 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +49 -15
- package/dist/node.d.ts +13 -1
- package/dist/node.js +43 -2
- package/dist/parsers/codexHistory.js +2 -2
- package/dist/parsers/codexParser.d.ts +12 -0
- package/dist/parsers/codexParser.js +34 -12
- package/dist/parsers/debugLogParser.js +2 -2
- package/dist/parsers/sessionPathResolver.js +5 -4
- package/dist/promptHistory.d.ts +32 -0
- package/dist/promptHistory.js +115 -59
- package/dist/providerFailure.d.ts +12 -6
- package/dist/providerFailure.js +45 -0
- package/dist/providers/claudeCode.js +7 -99
- package/dist/providers/claudeListing.d.ts +22 -0
- package/dist/providers/claudeListing.js +158 -0
- package/dist/providers/codex.d.ts +3 -1
- package/dist/providers/codex.js +26 -227
- package/dist/providers/codexRollouts.d.ts +44 -0
- package/dist/providers/codexRollouts.js +261 -0
- package/dist/providers/detect.js +4 -4
- package/dist/readers/plans.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/providerFailure.d.ts +154 -0
- package/dist/schemas/providerFailure.js +40 -0
- package/dist/sessionPromptHistory.d.ts +12 -7
- package/dist/sessionPromptHistory.js +15 -3
- package/dist/sessionPromptSignals.d.ts +9 -0
- package/dist/sessionPromptSignals.js +13 -0
- package/dist/testing/credentialGuard.d.ts +23 -0
- package/dist/testing/credentialGuard.js +233 -0
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.js +22 -0
- package/dist/testing/sessionLogFixtures.d.ts +28 -0
- package/dist/testing/sessionLogFixtures.js +169 -0
- package/dist/tokenEstimation.d.ts +2 -0
- package/dist/tokenEstimation.js +21 -11
- package/dist/toolCategory.d.ts +18 -0
- package/dist/toolCategory.js +86 -0
- package/dist/types/codex.d.ts +4 -0
- package/dist/usage/usageEvents.d.ts +16 -0
- package/dist/usage/usageEvents.js +53 -9
- package/dist/watchers/jsonlTail.d.ts +26 -1
- package/dist/watchers/jsonlTail.js +136 -28
- package/dist/watchers/jsonlWatcher.js +7 -0
- package/dist/watchers/providerReaderWatcher.js +1 -0
- package/dist/watchers/types.d.ts +6 -0
- package/dist/workspaceRoots.d.ts +23 -0
- package/dist/workspaceRoots.js +93 -0
- package/package.json +22 -2
|
@@ -0,0 +1,2034 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared event aggregation engine.
|
|
3
|
+
*
|
|
4
|
+
* Pure computation class (no I/O, no framework dependencies) that processes
|
|
5
|
+
* SessionEvent and FollowEvent objects to accumulate session metrics.
|
|
6
|
+
* Both the VS Code SessionMonitor and CLI DashboardState delegate to this.
|
|
7
|
+
*
|
|
8
|
+
* @module aggregation/EventAggregator
|
|
9
|
+
*/
|
|
10
|
+
import { TRUNCATION_PATTERNS } from '../parsers/jsonl';
|
|
11
|
+
import { PlanExtractor } from '../parsers/planExtractor';
|
|
12
|
+
import { toFollowEvents } from '../watchers/eventBridge';
|
|
13
|
+
import { formatToolSummary } from '../formatters/toolSummary';
|
|
14
|
+
import { isHardNoise, isHardNoiseFollowEvent, getSoftNoiseReason, classifyMessage, classifyFollowEvent, } from '../formatters/noiseClassifier';
|
|
15
|
+
import { classifyCostProvenance } from './costProvenance';
|
|
16
|
+
import { FrequencyTracker } from './FrequencyTracker';
|
|
17
|
+
import { HeatmapTracker } from './HeatmapTracker';
|
|
18
|
+
import { PatternExtractor } from './PatternExtractor';
|
|
19
|
+
import { categorizeError } from '../extractors/errorTaxonomy';
|
|
20
|
+
import { estimateTextTokens } from '../tokenEstimation';
|
|
21
|
+
import { calculateNormalizedUsageCost, extractNormalizedUsage, normalizeProviderUsage, } from '../usageNormalization';
|
|
22
|
+
// ── Defaults ──
|
|
23
|
+
const DEFAULT_TIMELINE_CAP = 200;
|
|
24
|
+
const DEFAULT_LATENCY_CAP = 100;
|
|
25
|
+
const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
|
|
26
|
+
const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
|
|
27
|
+
const DEFAULT_BURN_SAMPLE_MS = 10_000;
|
|
28
|
+
const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
|
|
29
|
+
/** Schema version for serialized snapshots. v6: token totals count each Claude response and Codex call once. */
|
|
30
|
+
export const SNAPSHOT_SCHEMA_VERSION = 6;
|
|
31
|
+
// ── Goal gate detection regex ──
|
|
32
|
+
const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
|
|
33
|
+
// ── Todo dependency parsing ──
|
|
34
|
+
/**
|
|
35
|
+
* Parses dependency references from a todo item's content text.
|
|
36
|
+
*
|
|
37
|
+
* Looks for patterns like "(blocked by Task A and Task B)" and maps
|
|
38
|
+
* referenced task names back to todo-{i} IDs by substring matching.
|
|
39
|
+
*/
|
|
40
|
+
export function parseTodoDependencies(content, allTodos) {
|
|
41
|
+
const depPattern = /(?:blocked by|depends on|waiting on|requires)\s+(.+?)(?:\)|$)/i;
|
|
42
|
+
const match = content.match(depPattern);
|
|
43
|
+
if (!match)
|
|
44
|
+
return [];
|
|
45
|
+
const refs = match[1]
|
|
46
|
+
.split(/\s+and\s+|,\s*|&\s*/)
|
|
47
|
+
.map((r) => r.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const result = [];
|
|
50
|
+
for (const ref of refs) {
|
|
51
|
+
const refLower = ref.toLowerCase();
|
|
52
|
+
for (let j = 0; j < allTodos.length; j++) {
|
|
53
|
+
const otherContent = String(allTodos[j].content || allTodos[j].subject || '').toLowerCase();
|
|
54
|
+
if (otherContent.includes(refLower) || refLower.includes(otherContent.split(':')[0].trim())) {
|
|
55
|
+
result.push(`todo-${j}`);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
// ── EventAggregator ──
|
|
63
|
+
export class EventAggregator {
|
|
64
|
+
// Options
|
|
65
|
+
timelineCap;
|
|
66
|
+
latencyCap;
|
|
67
|
+
burnWindowMs;
|
|
68
|
+
burnSampleMs;
|
|
69
|
+
computeContextSize;
|
|
70
|
+
providerId;
|
|
71
|
+
// Token totals
|
|
72
|
+
inputTokens = 0;
|
|
73
|
+
outputTokens = 0;
|
|
74
|
+
cacheWriteTokens = 0;
|
|
75
|
+
cacheReadTokens = 0;
|
|
76
|
+
reasoningTokens = 0;
|
|
77
|
+
totalTokens = 0;
|
|
78
|
+
/** Running cost in USD (reported + estimated). */
|
|
79
|
+
costUsd = 0;
|
|
80
|
+
reportedCostUsd = 0;
|
|
81
|
+
estimatedCostUsd = 0;
|
|
82
|
+
reportedCalls = 0;
|
|
83
|
+
estimatedCalls = 0;
|
|
84
|
+
unpricedCalls = 0;
|
|
85
|
+
// Per-model usage
|
|
86
|
+
modelUsage = new Map();
|
|
87
|
+
// Context size tracking
|
|
88
|
+
currentContextSize = 0;
|
|
89
|
+
previousContextSize = 0;
|
|
90
|
+
/**
|
|
91
|
+
* Index of a heuristic compaction event still awaiting its real
|
|
92
|
+
* post-compaction size from the next usage event. Prevents the usage-drop
|
|
93
|
+
* detector from double-counting an explicit compaction marker.
|
|
94
|
+
*/
|
|
95
|
+
pendingHeuristicCompactionIndex = null;
|
|
96
|
+
// Compaction
|
|
97
|
+
compactionEvents = [];
|
|
98
|
+
// Truncation
|
|
99
|
+
truncationEvents = [];
|
|
100
|
+
// Tool analytics
|
|
101
|
+
toolAnalytics = new Map();
|
|
102
|
+
pendingToolCalls = new Map();
|
|
103
|
+
errorByToolCategory = new Map();
|
|
104
|
+
errorBuckets = new Map();
|
|
105
|
+
retryAttempts = 0;
|
|
106
|
+
finishReasons = new Map();
|
|
107
|
+
// Burn rate
|
|
108
|
+
burnSamples = [];
|
|
109
|
+
lastBurnSampleTime = 0;
|
|
110
|
+
tokensSinceLastSample = 0;
|
|
111
|
+
// Task state
|
|
112
|
+
tasks = new Map();
|
|
113
|
+
pendingTaskCreates = new Map();
|
|
114
|
+
activeTaskId = null;
|
|
115
|
+
// Subagent lifecycle
|
|
116
|
+
subagents = [];
|
|
117
|
+
pendingSubagents = new Map(); // toolUseId -> index
|
|
118
|
+
// Plan state
|
|
119
|
+
planExtractor;
|
|
120
|
+
planState = null;
|
|
121
|
+
planEnteredAt;
|
|
122
|
+
// Context attribution
|
|
123
|
+
contextAttribution = {
|
|
124
|
+
systemPrompt: 0,
|
|
125
|
+
userMessages: 0,
|
|
126
|
+
assistantResponses: 0,
|
|
127
|
+
toolInputs: 0,
|
|
128
|
+
toolOutputs: 0,
|
|
129
|
+
thinking: 0,
|
|
130
|
+
other: 0,
|
|
131
|
+
};
|
|
132
|
+
// Permission mode
|
|
133
|
+
currentPermissionMode = null;
|
|
134
|
+
permissionModeHistory = [];
|
|
135
|
+
// Context timeline
|
|
136
|
+
contextTimeline = [];
|
|
137
|
+
contextTimelineCap;
|
|
138
|
+
contextTurnIndex = 0;
|
|
139
|
+
// Timeline
|
|
140
|
+
timeline = [];
|
|
141
|
+
// Latency tracking
|
|
142
|
+
pendingUserRequest = null;
|
|
143
|
+
latencyRecords = [];
|
|
144
|
+
// Counters
|
|
145
|
+
messageCount = 0;
|
|
146
|
+
eventCount = 0;
|
|
147
|
+
sessionStartTime = null;
|
|
148
|
+
lastEventTime = null;
|
|
149
|
+
currentModel = null;
|
|
150
|
+
_providerId = null;
|
|
151
|
+
// Gonzo/Lazyjournal analytics
|
|
152
|
+
toolFrequency = new FrequencyTracker();
|
|
153
|
+
wordFrequency = new FrequencyTracker();
|
|
154
|
+
patternExtractor = new PatternExtractor();
|
|
155
|
+
heatmapTracker = new HeatmapTracker();
|
|
156
|
+
constructor(options) {
|
|
157
|
+
this.timelineCap = options?.timelineCap ?? DEFAULT_TIMELINE_CAP;
|
|
158
|
+
this.latencyCap = options?.latencyCap ?? DEFAULT_LATENCY_CAP;
|
|
159
|
+
this.burnWindowMs = options?.burnWindowMs ?? DEFAULT_BURN_WINDOW_MS;
|
|
160
|
+
this.burnSampleMs = options?.burnSampleMs ?? DEFAULT_BURN_SAMPLE_MS;
|
|
161
|
+
this.contextTimelineCap = options?.contextTimelineCap ?? DEFAULT_CONTEXT_TIMELINE_CAP;
|
|
162
|
+
this.computeContextSize = options?.computeContextSize ?? null;
|
|
163
|
+
this.providerId = options?.providerId ?? null;
|
|
164
|
+
this._providerId = this.providerId;
|
|
165
|
+
this.planExtractor = new PlanExtractor(options?.readPlanFile);
|
|
166
|
+
}
|
|
167
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
168
|
+
// processEvent — main entry for SessionEvent
|
|
169
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
170
|
+
processEvent(event) {
|
|
171
|
+
// 1. Increment counters, capture timestamps
|
|
172
|
+
this.eventCount++;
|
|
173
|
+
if (!this.sessionStartTime) {
|
|
174
|
+
this.sessionStartTime = event.timestamp;
|
|
175
|
+
}
|
|
176
|
+
this.lastEventTime = event.timestamp;
|
|
177
|
+
this.recordProviderMetadata(event);
|
|
178
|
+
// Bookkeeping rows can be useful without a message payload.
|
|
179
|
+
if (!event.message) {
|
|
180
|
+
if (event.type === 'summary') {
|
|
181
|
+
this.recordExplicitCompaction(event.timestamp, event.compaction);
|
|
182
|
+
}
|
|
183
|
+
this.trackPermissionMode(event);
|
|
184
|
+
this.addTimelineFromSessionEvent(event);
|
|
185
|
+
this.feedAnalytics(event.timestamp, event.type === 'summary' ? (event.summary ?? '') : '', this.extractToolNameFromEvent(event));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// 2. Track model
|
|
189
|
+
if (event.message.model) {
|
|
190
|
+
this.currentModel = event.message.model;
|
|
191
|
+
}
|
|
192
|
+
// 3. Skip system/audit events and synthetic token-count events for messageCount
|
|
193
|
+
const msgId = event.message.id ?? '';
|
|
194
|
+
if ((event.type === 'user' || event.type === 'assistant') &&
|
|
195
|
+
!msgId.startsWith('token-count-')) {
|
|
196
|
+
this.messageCount++;
|
|
197
|
+
}
|
|
198
|
+
// 4. Latency tracking
|
|
199
|
+
if (event.type !== 'system') {
|
|
200
|
+
this.processLatency(event);
|
|
201
|
+
}
|
|
202
|
+
// 5. Token accumulation
|
|
203
|
+
const normalizedUsage = extractNormalizedUsage(event);
|
|
204
|
+
if (normalizedUsage) {
|
|
205
|
+
const usageCost = this.accumulateUsage(normalizedUsage, event.timestamp, event.message.model, event.message.usageKind);
|
|
206
|
+
this.attributePlanUsage(normalizedUsage.totalTokens, usageCost);
|
|
207
|
+
}
|
|
208
|
+
if (event.type !== 'system') {
|
|
209
|
+
// 6. Tool extraction from content blocks
|
|
210
|
+
this.extractToolsFromContent(event);
|
|
211
|
+
// 7. Task state
|
|
212
|
+
this.extractTaskStateFromEvent(event);
|
|
213
|
+
// 8. Subagent tracking
|
|
214
|
+
this.extractSubagentFromEvent(event);
|
|
215
|
+
// 9. Plan extraction — convert SessionEvent to a FollowEvent shape for PlanExtractor
|
|
216
|
+
this.extractPlanFromSessionEvent(event);
|
|
217
|
+
}
|
|
218
|
+
// 10. Context attribution
|
|
219
|
+
this.attributeContextFromEvent(event);
|
|
220
|
+
// 11. Permission mode tracking
|
|
221
|
+
this.trackPermissionMode(event);
|
|
222
|
+
if (event.type === 'summary') {
|
|
223
|
+
this.recordExplicitCompaction(event.timestamp, event.compaction);
|
|
224
|
+
}
|
|
225
|
+
// 12. Timeline
|
|
226
|
+
this.addTimelineFromSessionEvent(event);
|
|
227
|
+
// 13. Gonzo/Lazyjournal analytics
|
|
228
|
+
const summary = this.extractTextContent(event) ?? '';
|
|
229
|
+
const toolName = this.extractToolNameFromEvent(event);
|
|
230
|
+
this.feedAnalytics(event.timestamp, summary, toolName);
|
|
231
|
+
}
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
233
|
+
// processFollowEvent — adapter for CLI FollowEvent format
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
235
|
+
processFollowEvent(event) {
|
|
236
|
+
// 1. Increment counters, capture timestamps
|
|
237
|
+
this.eventCount++;
|
|
238
|
+
if (!this.sessionStartTime) {
|
|
239
|
+
this.sessionStartTime = event.timestamp;
|
|
240
|
+
}
|
|
241
|
+
this.lastEventTime = event.timestamp;
|
|
242
|
+
// Provider
|
|
243
|
+
if (!this._providerId && event.providerId) {
|
|
244
|
+
this._providerId = event.providerId;
|
|
245
|
+
}
|
|
246
|
+
// 2. Model tracking
|
|
247
|
+
if (event.model) {
|
|
248
|
+
this.currentModel = event.model;
|
|
249
|
+
}
|
|
250
|
+
// 3. Token accumulation from FollowEvent fields
|
|
251
|
+
if (event.tokens) {
|
|
252
|
+
const normalized = normalizeProviderUsage({
|
|
253
|
+
semantics: 'sidekick',
|
|
254
|
+
provider: event.providerId,
|
|
255
|
+
source: 'follow-event',
|
|
256
|
+
model: event.model,
|
|
257
|
+
inputTokens: event.tokens.input,
|
|
258
|
+
outputTokens: event.tokens.output,
|
|
259
|
+
cacheReadTokens: event.cacheTokens?.read,
|
|
260
|
+
cacheWriteTokens: event.cacheTokens?.write,
|
|
261
|
+
reasoningIncludedInOutput: false,
|
|
262
|
+
reportedCostUsd: event.cost,
|
|
263
|
+
});
|
|
264
|
+
const cost = this.accumulateUsage(normalized, event.timestamp, event.model, event.usageKind);
|
|
265
|
+
this.attributePlanUsage(normalized.totalTokens, cost);
|
|
266
|
+
}
|
|
267
|
+
else if (typeof event.cost === 'number' && Number.isFinite(event.cost) && event.cost >= 0) {
|
|
268
|
+
// Compatibility for cost-only follow events (provider-reported by definition).
|
|
269
|
+
this.costUsd += event.cost;
|
|
270
|
+
this.reportedCostUsd += event.cost;
|
|
271
|
+
this.reportedCalls += 1;
|
|
272
|
+
}
|
|
273
|
+
// 4. Tool tracking from FollowEvent
|
|
274
|
+
if (event.type === 'tool_use' && event.toolName) {
|
|
275
|
+
this.recordFollowToolUse(event);
|
|
276
|
+
this.attributePlanToolCall(event.toolName);
|
|
277
|
+
}
|
|
278
|
+
else if (event.type === 'tool_result') {
|
|
279
|
+
this.recordFollowToolResult(event);
|
|
280
|
+
}
|
|
281
|
+
// 5. Task state extraction
|
|
282
|
+
this.extractTaskStateFromFollowEvent(event);
|
|
283
|
+
// 6. Subagent tracking
|
|
284
|
+
this.extractSubagentFromFollowEvent(event);
|
|
285
|
+
// 7. Plan extraction
|
|
286
|
+
this.processPlanFollowEvent(event);
|
|
287
|
+
// 8. Context attribution
|
|
288
|
+
this.attributeContextFromFollowEvent(event);
|
|
289
|
+
// 9. Truncation detection from tool results
|
|
290
|
+
if (event.type === 'tool_result') {
|
|
291
|
+
this.detectTruncationFromFollowEvent(event);
|
|
292
|
+
}
|
|
293
|
+
// 10. Explicit compaction event (summary type)
|
|
294
|
+
if (event.type === 'summary') {
|
|
295
|
+
this.recordExplicitCompaction(event.timestamp, event.compaction);
|
|
296
|
+
}
|
|
297
|
+
// 11. Timeline
|
|
298
|
+
this.addTimelineFromFollowEvent(event);
|
|
299
|
+
// 12. Gonzo/Lazyjournal analytics
|
|
300
|
+
this.feedAnalytics(event.timestamp, event.summary, event.toolName);
|
|
301
|
+
// Message count (skip system events)
|
|
302
|
+
if (event.type === 'user' || event.type === 'assistant') {
|
|
303
|
+
this.messageCount++;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
307
|
+
// Public getters
|
|
308
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
309
|
+
getAggregatedTokens() {
|
|
310
|
+
return {
|
|
311
|
+
inputTokens: this.inputTokens,
|
|
312
|
+
outputTokens: this.outputTokens,
|
|
313
|
+
cacheWriteTokens: this.cacheWriteTokens,
|
|
314
|
+
cacheReadTokens: this.cacheReadTokens,
|
|
315
|
+
reasoningTokens: this.reasoningTokens,
|
|
316
|
+
totalTokens: this.totalTokens,
|
|
317
|
+
costUsd: this.costUsd,
|
|
318
|
+
reportedCostUsd: this.reportedCostUsd,
|
|
319
|
+
estimatedCostUsd: this.estimatedCostUsd,
|
|
320
|
+
unpricedCalls: this.unpricedCalls,
|
|
321
|
+
costProvenance: classifyCostProvenance({
|
|
322
|
+
reportedCalls: this.reportedCalls,
|
|
323
|
+
estimatedCalls: this.estimatedCalls,
|
|
324
|
+
unpricedCalls: this.unpricedCalls,
|
|
325
|
+
}),
|
|
326
|
+
reportedCost: this.costUsd,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
getModelStats() {
|
|
330
|
+
return Array.from(this.modelUsage.entries())
|
|
331
|
+
.map(([model, acc]) => ({ model, ...acc }))
|
|
332
|
+
.sort((a, b) => b.calls - a.calls);
|
|
333
|
+
}
|
|
334
|
+
getToolStats() {
|
|
335
|
+
return Array.from(this.toolAnalytics.values()).sort((a, b) => b.successCount +
|
|
336
|
+
b.failureCount +
|
|
337
|
+
b.pendingCount -
|
|
338
|
+
(a.successCount + a.failureCount + a.pendingCount));
|
|
339
|
+
}
|
|
340
|
+
getErrorRollup() {
|
|
341
|
+
return {
|
|
342
|
+
totalFailures: Array.from(this.errorByToolCategory.values()).reduce((sum, entry) => sum + entry.count, 0),
|
|
343
|
+
byToolCategory: Array.from(this.errorByToolCategory.values()).sort((a, b) => b.count - a.count),
|
|
344
|
+
byHourModel: Array.from(this.errorBuckets.values()).sort((a, b) => a.hour.localeCompare(b.hour)),
|
|
345
|
+
retryAttempts: this.retryAttempts,
|
|
346
|
+
finishReasons: Array.from(this.finishReasons, ([reason, count]) => ({ reason, count })).sort((a, b) => b.count - a.count),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
getCompactionEvents() {
|
|
350
|
+
return [...this.compactionEvents];
|
|
351
|
+
}
|
|
352
|
+
getTruncationEvents() {
|
|
353
|
+
return [...this.truncationEvents];
|
|
354
|
+
}
|
|
355
|
+
getBurnRate() {
|
|
356
|
+
const points = this.burnSamples.map((s) => s.tokens);
|
|
357
|
+
const tokensPerMinute = points.length > 0 ? points[points.length - 1] : 0;
|
|
358
|
+
return {
|
|
359
|
+
tokensPerMinute,
|
|
360
|
+
points: [...points],
|
|
361
|
+
sampleCount: this.burnSamples.length,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
getTaskState() {
|
|
365
|
+
return {
|
|
366
|
+
tasks: new Map(this.tasks),
|
|
367
|
+
activeTaskId: this.activeTaskId,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
getSubagents() {
|
|
371
|
+
return [...this.subagents];
|
|
372
|
+
}
|
|
373
|
+
getPlan() {
|
|
374
|
+
return this.planState
|
|
375
|
+
? { ...this.planState, steps: this.planState.steps.map((step) => ({ ...step })) }
|
|
376
|
+
: null;
|
|
377
|
+
}
|
|
378
|
+
getContextAttribution() {
|
|
379
|
+
return { ...this.contextAttribution };
|
|
380
|
+
}
|
|
381
|
+
getTimeline() {
|
|
382
|
+
return [...this.timeline];
|
|
383
|
+
}
|
|
384
|
+
getLatencyStats() {
|
|
385
|
+
if (this.latencyRecords.length === 0)
|
|
386
|
+
return null;
|
|
387
|
+
const records = this.latencyRecords;
|
|
388
|
+
const totalFirstToken = records.reduce((sum, r) => sum + r.firstTokenLatencyMs, 0);
|
|
389
|
+
const totalResponse = records.reduce((sum, r) => sum + r.totalResponseTimeMs, 0);
|
|
390
|
+
const maxFirstToken = Math.max(...records.map((r) => r.firstTokenLatencyMs));
|
|
391
|
+
const last = records[records.length - 1];
|
|
392
|
+
return {
|
|
393
|
+
recentLatencies: [...records],
|
|
394
|
+
avgFirstTokenLatencyMs: Math.round(totalFirstToken / records.length),
|
|
395
|
+
maxFirstTokenLatencyMs: maxFirstToken,
|
|
396
|
+
avgTotalResponseTimeMs: Math.round(totalResponse / records.length),
|
|
397
|
+
lastFirstTokenLatencyMs: last ? last.firstTokenLatencyMs : null,
|
|
398
|
+
completedCycles: records.length,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
getMetrics() {
|
|
402
|
+
return {
|
|
403
|
+
sessionStartTime: this.sessionStartTime,
|
|
404
|
+
lastEventTime: this.lastEventTime,
|
|
405
|
+
messageCount: this.messageCount,
|
|
406
|
+
eventCount: this.eventCount,
|
|
407
|
+
currentModel: this.currentModel,
|
|
408
|
+
providerId: this._providerId,
|
|
409
|
+
tokens: this.getAggregatedTokens(),
|
|
410
|
+
modelStats: this.getModelStats(),
|
|
411
|
+
currentContextSize: this.currentContextSize,
|
|
412
|
+
contextAttribution: this.getContextAttribution(),
|
|
413
|
+
compactionCount: this.compactionEvents.length,
|
|
414
|
+
compactionEvents: this.getCompactionEvents(),
|
|
415
|
+
truncationCount: this.truncationEvents.length,
|
|
416
|
+
truncationEvents: this.getTruncationEvents(),
|
|
417
|
+
toolStats: this.getToolStats(),
|
|
418
|
+
errorRollup: this.getErrorRollup(),
|
|
419
|
+
burnRate: this.getBurnRate(),
|
|
420
|
+
taskState: this.getTaskState(),
|
|
421
|
+
subagents: this.getSubagents(),
|
|
422
|
+
plan: this.getPlan(),
|
|
423
|
+
permissionMode: this.currentPermissionMode,
|
|
424
|
+
permissionModeHistory: [...this.permissionModeHistory],
|
|
425
|
+
contextTimeline: [...this.contextTimeline],
|
|
426
|
+
timeline: this.getTimeline(),
|
|
427
|
+
latencyStats: this.getLatencyStats(),
|
|
428
|
+
toolFrequency: this.toolFrequency.getTopN(20).map((e) => ({ name: e.key, count: e.count })),
|
|
429
|
+
wordFrequency: this.wordFrequency.getTopN(20).map((e) => ({ name: e.key, count: e.count })),
|
|
430
|
+
patterns: this.patternExtractor.getPatterns().slice(0, 20),
|
|
431
|
+
heatmapBuckets: this.heatmapTracker.getBuckets(),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
435
|
+
// reset — clear all state
|
|
436
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
437
|
+
reset() {
|
|
438
|
+
this.inputTokens = 0;
|
|
439
|
+
this.outputTokens = 0;
|
|
440
|
+
this.cacheWriteTokens = 0;
|
|
441
|
+
this.cacheReadTokens = 0;
|
|
442
|
+
this.reasoningTokens = 0;
|
|
443
|
+
this.totalTokens = 0;
|
|
444
|
+
this.costUsd = 0;
|
|
445
|
+
this.reportedCostUsd = 0;
|
|
446
|
+
this.estimatedCostUsd = 0;
|
|
447
|
+
this.reportedCalls = 0;
|
|
448
|
+
this.estimatedCalls = 0;
|
|
449
|
+
this.unpricedCalls = 0;
|
|
450
|
+
this.modelUsage.clear();
|
|
451
|
+
this.currentContextSize = 0;
|
|
452
|
+
this.previousContextSize = 0;
|
|
453
|
+
this.compactionEvents = [];
|
|
454
|
+
this.truncationEvents = [];
|
|
455
|
+
this.toolAnalytics.clear();
|
|
456
|
+
this.pendingToolCalls.clear();
|
|
457
|
+
this.errorByToolCategory.clear();
|
|
458
|
+
this.errorBuckets.clear();
|
|
459
|
+
this.retryAttempts = 0;
|
|
460
|
+
this.finishReasons.clear();
|
|
461
|
+
this.burnSamples = [];
|
|
462
|
+
this.lastBurnSampleTime = 0;
|
|
463
|
+
this.tokensSinceLastSample = 0;
|
|
464
|
+
this.tasks.clear();
|
|
465
|
+
this.pendingTaskCreates.clear();
|
|
466
|
+
this.activeTaskId = null;
|
|
467
|
+
this.subagents = [];
|
|
468
|
+
this.pendingSubagents.clear();
|
|
469
|
+
this.planExtractor.reset();
|
|
470
|
+
this.planState = null;
|
|
471
|
+
this.planEnteredAt = undefined;
|
|
472
|
+
this.currentPermissionMode = null;
|
|
473
|
+
this.permissionModeHistory = [];
|
|
474
|
+
this.contextTimeline = [];
|
|
475
|
+
this.contextTurnIndex = 0;
|
|
476
|
+
this.contextAttribution = {
|
|
477
|
+
systemPrompt: 0,
|
|
478
|
+
userMessages: 0,
|
|
479
|
+
assistantResponses: 0,
|
|
480
|
+
toolInputs: 0,
|
|
481
|
+
toolOutputs: 0,
|
|
482
|
+
thinking: 0,
|
|
483
|
+
other: 0,
|
|
484
|
+
};
|
|
485
|
+
this.timeline = [];
|
|
486
|
+
this.pendingUserRequest = null;
|
|
487
|
+
this.latencyRecords = [];
|
|
488
|
+
this.toolFrequency.reset();
|
|
489
|
+
this.wordFrequency.reset();
|
|
490
|
+
this.patternExtractor.reset();
|
|
491
|
+
this.heatmapTracker.reset();
|
|
492
|
+
this.messageCount = 0;
|
|
493
|
+
this.eventCount = 0;
|
|
494
|
+
this.sessionStartTime = null;
|
|
495
|
+
this.lastEventTime = null;
|
|
496
|
+
this.currentModel = null;
|
|
497
|
+
this._providerId = this.providerId;
|
|
498
|
+
}
|
|
499
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
500
|
+
// Seed methods — for initializing state from provider snapshots
|
|
501
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
502
|
+
/** Seeds the current context size (e.g., from a provider usage snapshot on attach). */
|
|
503
|
+
seedContextSize(size) {
|
|
504
|
+
this.currentContextSize = size;
|
|
505
|
+
this.previousContextSize = size;
|
|
506
|
+
}
|
|
507
|
+
/** Seeds context attribution (e.g., from a provider's DB-backed attribution). */
|
|
508
|
+
seedContextAttribution(attribution) {
|
|
509
|
+
this.contextAttribution = { ...attribution };
|
|
510
|
+
}
|
|
511
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
512
|
+
// Snapshot serialization — for fast session resume
|
|
513
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
514
|
+
/** Serializes all mutable state to a JSON-safe object for snapshot persistence. */
|
|
515
|
+
serialize() {
|
|
516
|
+
return {
|
|
517
|
+
version: SNAPSHOT_SCHEMA_VERSION,
|
|
518
|
+
tokens: {
|
|
519
|
+
input: this.inputTokens,
|
|
520
|
+
output: this.outputTokens,
|
|
521
|
+
cacheWrite: this.cacheWriteTokens,
|
|
522
|
+
cacheRead: this.cacheReadTokens,
|
|
523
|
+
reasoning: this.reasoningTokens,
|
|
524
|
+
total: this.totalTokens,
|
|
525
|
+
reportedCost: this.costUsd,
|
|
526
|
+
costUsd: this.costUsd,
|
|
527
|
+
reportedCostUsd: this.reportedCostUsd,
|
|
528
|
+
estimatedCostUsd: this.estimatedCostUsd,
|
|
529
|
+
reportedCalls: this.reportedCalls,
|
|
530
|
+
estimatedCalls: this.estimatedCalls,
|
|
531
|
+
unpricedCalls: this.unpricedCalls,
|
|
532
|
+
},
|
|
533
|
+
modelUsage: Array.from(this.modelUsage.entries()),
|
|
534
|
+
contextSize: this.currentContextSize,
|
|
535
|
+
previousContextSize: this.previousContextSize,
|
|
536
|
+
compactionEvents: this.compactionEvents,
|
|
537
|
+
truncationEvents: this.truncationEvents,
|
|
538
|
+
toolAnalytics: Array.from(this.toolAnalytics.entries()),
|
|
539
|
+
errorByToolCategory: Array.from(this.errorByToolCategory.entries()),
|
|
540
|
+
errorBuckets: Array.from(this.errorBuckets.entries()),
|
|
541
|
+
retryAttempts: this.retryAttempts,
|
|
542
|
+
finishReasons: Array.from(this.finishReasons.entries()),
|
|
543
|
+
contextAttribution: { ...this.contextAttribution },
|
|
544
|
+
burnSamples: [...this.burnSamples],
|
|
545
|
+
lastBurnSampleTime: this.lastBurnSampleTime,
|
|
546
|
+
tokensSinceLastSample: this.tokensSinceLastSample,
|
|
547
|
+
latencyRecords: [...this.latencyRecords],
|
|
548
|
+
tasks: Array.from(this.tasks.entries()),
|
|
549
|
+
activeTaskId: this.activeTaskId,
|
|
550
|
+
subagents: [...this.subagents],
|
|
551
|
+
permissionMode: this.currentPermissionMode,
|
|
552
|
+
permissionModeHistory: [...this.permissionModeHistory],
|
|
553
|
+
contextTimeline: [...this.contextTimeline],
|
|
554
|
+
contextTurnIndex: this.contextTurnIndex,
|
|
555
|
+
timeline: [...this.timeline],
|
|
556
|
+
messageCount: this.messageCount,
|
|
557
|
+
eventCount: this.eventCount,
|
|
558
|
+
sessionStartTime: this.sessionStartTime,
|
|
559
|
+
lastEventTime: this.lastEventTime,
|
|
560
|
+
currentModel: this.currentModel,
|
|
561
|
+
toolFrequency: this.toolFrequency.serialize(),
|
|
562
|
+
wordFrequency: this.wordFrequency.serialize(),
|
|
563
|
+
patternState: this.patternExtractor.serialize(),
|
|
564
|
+
heatmapState: this.heatmapTracker.serialize(),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
|
|
568
|
+
restore(state) {
|
|
569
|
+
if (state.version !== SNAPSHOT_SCHEMA_VERSION) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
this.inputTokens = state.tokens.input;
|
|
573
|
+
this.outputTokens = state.tokens.output;
|
|
574
|
+
this.cacheWriteTokens = state.tokens.cacheWrite;
|
|
575
|
+
this.cacheReadTokens = state.tokens.cacheRead;
|
|
576
|
+
this.reasoningTokens = state.tokens.reasoning;
|
|
577
|
+
this.totalTokens = state.tokens.total;
|
|
578
|
+
this.costUsd = state.tokens.costUsd;
|
|
579
|
+
this.reportedCostUsd = state.tokens.reportedCostUsd;
|
|
580
|
+
this.estimatedCostUsd = state.tokens.estimatedCostUsd;
|
|
581
|
+
this.reportedCalls = state.tokens.reportedCalls;
|
|
582
|
+
this.estimatedCalls = state.tokens.estimatedCalls;
|
|
583
|
+
this.unpricedCalls = state.tokens.unpricedCalls;
|
|
584
|
+
this.modelUsage = new Map(state.modelUsage);
|
|
585
|
+
this.currentContextSize = state.contextSize;
|
|
586
|
+
this.previousContextSize = state.previousContextSize;
|
|
587
|
+
this.compactionEvents = [...state.compactionEvents];
|
|
588
|
+
this.truncationEvents = [...state.truncationEvents];
|
|
589
|
+
this.toolAnalytics = new Map(state.toolAnalytics);
|
|
590
|
+
this.errorByToolCategory = new Map(state.errorByToolCategory);
|
|
591
|
+
this.errorBuckets = new Map(state.errorBuckets);
|
|
592
|
+
this.retryAttempts = state.retryAttempts;
|
|
593
|
+
this.finishReasons = new Map(state.finishReasons);
|
|
594
|
+
this.contextAttribution = { ...state.contextAttribution };
|
|
595
|
+
this.burnSamples = [...state.burnSamples];
|
|
596
|
+
this.lastBurnSampleTime = state.lastBurnSampleTime;
|
|
597
|
+
this.tokensSinceLastSample = state.tokensSinceLastSample;
|
|
598
|
+
this.latencyRecords = [...state.latencyRecords];
|
|
599
|
+
this.tasks = new Map(state.tasks);
|
|
600
|
+
this.activeTaskId = state.activeTaskId;
|
|
601
|
+
this.subagents = [...state.subagents];
|
|
602
|
+
this.currentPermissionMode = state.permissionMode ?? null;
|
|
603
|
+
this.permissionModeHistory = state.permissionModeHistory
|
|
604
|
+
? [...state.permissionModeHistory]
|
|
605
|
+
: [];
|
|
606
|
+
this.contextTimeline = state.contextTimeline
|
|
607
|
+
? state.contextTimeline.slice(-this.contextTimelineCap)
|
|
608
|
+
: [];
|
|
609
|
+
this.contextTurnIndex = state.contextTurnIndex ?? 0;
|
|
610
|
+
this.timeline = [...state.timeline];
|
|
611
|
+
this.messageCount = state.messageCount;
|
|
612
|
+
this.eventCount = state.eventCount;
|
|
613
|
+
this.sessionStartTime = state.sessionStartTime;
|
|
614
|
+
this.lastEventTime = state.lastEventTime;
|
|
615
|
+
this.currentModel = state.currentModel;
|
|
616
|
+
// Gonzo/Lazyjournal analytics restore
|
|
617
|
+
if (state.toolFrequency)
|
|
618
|
+
this.toolFrequency.restore(state.toolFrequency);
|
|
619
|
+
if (state.wordFrequency)
|
|
620
|
+
this.wordFrequency.restore(state.wordFrequency);
|
|
621
|
+
if (state.patternState)
|
|
622
|
+
this.patternExtractor.restore(state.patternState);
|
|
623
|
+
if (state.heatmapState)
|
|
624
|
+
this.heatmapTracker.restore(state.heatmapState);
|
|
625
|
+
// Clear transient state — pending calls won't survive a snapshot boundary
|
|
626
|
+
this.pendingToolCalls.clear();
|
|
627
|
+
this.pendingTaskCreates.clear();
|
|
628
|
+
this.pendingSubagents.clear();
|
|
629
|
+
this.pendingUserRequest = null;
|
|
630
|
+
this.planExtractor.reset();
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
634
|
+
// Private: Token & Context
|
|
635
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
636
|
+
/**
|
|
637
|
+
* Add one usage record to the running totals. A `'correction'` tops up a call
|
|
638
|
+
* already counted (a later split line of the same response grew), so it adds
|
|
639
|
+
* tokens and cost but no call, context sample, or compaction check.
|
|
640
|
+
*/
|
|
641
|
+
accumulateUsage(usage, timestamp, model, kind = 'call') {
|
|
642
|
+
const isCall = kind !== 'correction';
|
|
643
|
+
const inputTok = usage.uncachedInputTokens;
|
|
644
|
+
const outputTok = usage.outputTokens;
|
|
645
|
+
const cacheWrite = usage.cacheWriteTokens;
|
|
646
|
+
const cacheRead = usage.cacheReadTokens;
|
|
647
|
+
const reasoningTok = usage.reasoningTokens;
|
|
648
|
+
const modelKey = model ?? this.currentModel;
|
|
649
|
+
const pricedUsage = calculateNormalizedUsageCost({ usage, modelId: modelKey ?? undefined });
|
|
650
|
+
const priced = pricedUsage.source !== 'unpriced';
|
|
651
|
+
const cost = pricedUsage.costUsd ?? 0;
|
|
652
|
+
this.inputTokens += inputTok;
|
|
653
|
+
this.outputTokens += outputTok;
|
|
654
|
+
this.cacheWriteTokens += cacheWrite;
|
|
655
|
+
this.cacheReadTokens += cacheRead;
|
|
656
|
+
this.reasoningTokens += reasoningTok;
|
|
657
|
+
this.totalTokens += usage.totalTokens;
|
|
658
|
+
this.costUsd += cost;
|
|
659
|
+
const callIncrement = isCall ? 1 : 0;
|
|
660
|
+
if (pricedUsage.source === 'provider-reported') {
|
|
661
|
+
this.reportedCostUsd += cost;
|
|
662
|
+
this.reportedCalls += callIncrement;
|
|
663
|
+
}
|
|
664
|
+
else if (pricedUsage.source === 'unpriced') {
|
|
665
|
+
this.unpricedCalls += callIncrement;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
this.estimatedCostUsd += cost;
|
|
669
|
+
this.estimatedCalls += callIncrement;
|
|
670
|
+
}
|
|
671
|
+
// Burn rate sample accumulation
|
|
672
|
+
this.tokensSinceLastSample += usage.totalTokens;
|
|
673
|
+
if (!isCall) {
|
|
674
|
+
this.accumulateModelUsage(usage, model, cost, priced, false);
|
|
675
|
+
this.updateBurnRate(timestamp);
|
|
676
|
+
return cost;
|
|
677
|
+
}
|
|
678
|
+
// Context size computation
|
|
679
|
+
let contextSize;
|
|
680
|
+
if (this.computeContextSize) {
|
|
681
|
+
contextSize = this.computeContextSize({
|
|
682
|
+
inputTokens: inputTok,
|
|
683
|
+
outputTokens: outputTok,
|
|
684
|
+
cacheWriteTokens: cacheWrite,
|
|
685
|
+
cacheReadTokens: cacheRead,
|
|
686
|
+
reasoningTokens: reasoningTok,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
contextSize = inputTok + cacheWrite + cacheRead;
|
|
691
|
+
}
|
|
692
|
+
// Compaction detection
|
|
693
|
+
if (this.resolvePendingHeuristicCompaction(contextSize)) {
|
|
694
|
+
// The drop belongs to the explicit compaction marker just recorded.
|
|
695
|
+
}
|
|
696
|
+
else if (this.previousContextSize > 0 &&
|
|
697
|
+
contextSize < this.previousContextSize * COMPACTION_DROP_THRESHOLD) {
|
|
698
|
+
this.compactionEvents.push({
|
|
699
|
+
timestamp: new Date(timestamp),
|
|
700
|
+
contextBefore: this.previousContextSize,
|
|
701
|
+
contextAfter: contextSize,
|
|
702
|
+
tokensReclaimed: this.previousContextSize - contextSize,
|
|
703
|
+
source: 'heuristic',
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
this.previousContextSize = contextSize;
|
|
707
|
+
this.currentContextSize = contextSize;
|
|
708
|
+
// Context timeline tracking
|
|
709
|
+
this.contextTimeline.push({
|
|
710
|
+
timestamp,
|
|
711
|
+
inputTokens: contextSize,
|
|
712
|
+
turnIndex: this.contextTurnIndex++,
|
|
713
|
+
});
|
|
714
|
+
if (this.contextTimeline.length > this.contextTimelineCap) {
|
|
715
|
+
this.contextTimeline.splice(0, this.contextTimeline.length - this.contextTimelineCap);
|
|
716
|
+
}
|
|
717
|
+
this.accumulateModelUsage(usage, model, cost, priced, true);
|
|
718
|
+
// Burn rate sampling
|
|
719
|
+
this.updateBurnRate(timestamp);
|
|
720
|
+
return cost;
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Per-model usage. Accumulate reasoning tokens and an inverted-sticky
|
|
724
|
+
* `priced` flag so one unpriced event taints the aggregate (UI renders "—").
|
|
725
|
+
*/
|
|
726
|
+
accumulateModelUsage(usage, model, cost, priced, countCall) {
|
|
727
|
+
const inputTok = usage.uncachedInputTokens;
|
|
728
|
+
const outputTok = usage.outputTokens;
|
|
729
|
+
const cacheWrite = usage.cacheWriteTokens;
|
|
730
|
+
const cacheRead = usage.cacheReadTokens;
|
|
731
|
+
const reasoningTok = usage.reasoningTokens;
|
|
732
|
+
const perModelKey = model ?? this.currentModel ?? 'unknown';
|
|
733
|
+
const acc = this.modelUsage.get(perModelKey) ?? {
|
|
734
|
+
calls: 0,
|
|
735
|
+
tokens: 0,
|
|
736
|
+
inputTokens: 0,
|
|
737
|
+
outputTokens: 0,
|
|
738
|
+
cacheWriteTokens: 0,
|
|
739
|
+
cacheReadTokens: 0,
|
|
740
|
+
reasoningTokens: 0,
|
|
741
|
+
cost: 0,
|
|
742
|
+
priced: true,
|
|
743
|
+
};
|
|
744
|
+
if (countCall)
|
|
745
|
+
acc.calls++;
|
|
746
|
+
acc.tokens += usage.totalTokens;
|
|
747
|
+
acc.inputTokens += inputTok;
|
|
748
|
+
acc.outputTokens += outputTok;
|
|
749
|
+
acc.cacheWriteTokens += cacheWrite;
|
|
750
|
+
acc.cacheReadTokens += cacheRead;
|
|
751
|
+
acc.reasoningTokens = (acc.reasoningTokens ?? 0) + reasoningTok;
|
|
752
|
+
acc.cost += cost;
|
|
753
|
+
if (!priced)
|
|
754
|
+
acc.priced = false;
|
|
755
|
+
this.modelUsage.set(perModelKey, acc);
|
|
756
|
+
}
|
|
757
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
758
|
+
// Private: Burn Rate
|
|
759
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
760
|
+
updateBurnRate(timestamp) {
|
|
761
|
+
const now = new Date(timestamp).getTime();
|
|
762
|
+
if (isNaN(now))
|
|
763
|
+
return;
|
|
764
|
+
if (this.lastBurnSampleTime === 0) {
|
|
765
|
+
this.lastBurnSampleTime = now;
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
const elapsed = now - this.lastBurnSampleTime;
|
|
769
|
+
if (elapsed >= this.burnSampleMs) {
|
|
770
|
+
const tokPerMin = elapsed > 0 ? Math.round((this.tokensSinceLastSample / elapsed) * 60_000) : 0;
|
|
771
|
+
this.burnSamples.push({ time: now, tokens: tokPerMin });
|
|
772
|
+
this.tokensSinceLastSample = 0;
|
|
773
|
+
this.lastBurnSampleTime = now;
|
|
774
|
+
// Trim to window
|
|
775
|
+
const cutoff = now - this.burnWindowMs;
|
|
776
|
+
this.burnSamples = this.burnSamples.filter((s) => s.time >= cutoff);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
780
|
+
// Private: Latency Tracking (SessionEvent only)
|
|
781
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
782
|
+
processLatency(event) {
|
|
783
|
+
const now = new Date(event.timestamp);
|
|
784
|
+
if (event.type === 'user' && this.hasTextContent(event)) {
|
|
785
|
+
// User event with text content -> start tracking
|
|
786
|
+
this.pendingUserRequest = {
|
|
787
|
+
timestamp: now,
|
|
788
|
+
firstResponseReceived: false,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
else if (event.type === 'assistant' && this.pendingUserRequest) {
|
|
792
|
+
if (!this.pendingUserRequest.firstResponseReceived && this.hasTextContent(event)) {
|
|
793
|
+
// First assistant response with text
|
|
794
|
+
this.pendingUserRequest.firstResponseReceived = true;
|
|
795
|
+
this.pendingUserRequest.firstResponseTimestamp = now;
|
|
796
|
+
this.pendingUserRequest.firstTokenLatencyMs =
|
|
797
|
+
now.getTime() - this.pendingUserRequest.timestamp.getTime();
|
|
798
|
+
}
|
|
799
|
+
if (event.message.usage && this.pendingUserRequest.firstResponseReceived) {
|
|
800
|
+
// Assistant with usage -> complete the cycle
|
|
801
|
+
const totalResponseTimeMs = now.getTime() - this.pendingUserRequest.timestamp.getTime();
|
|
802
|
+
const firstTokenLatencyMs = this.pendingUserRequest.firstTokenLatencyMs ?? totalResponseTimeMs;
|
|
803
|
+
this.latencyRecords.push({
|
|
804
|
+
firstTokenLatencyMs,
|
|
805
|
+
totalResponseTimeMs,
|
|
806
|
+
requestTimestamp: this.pendingUserRequest.timestamp,
|
|
807
|
+
});
|
|
808
|
+
// Cap latency records
|
|
809
|
+
if (this.latencyRecords.length > this.latencyCap) {
|
|
810
|
+
this.latencyRecords.shift();
|
|
811
|
+
}
|
|
812
|
+
this.pendingUserRequest = null;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
817
|
+
// Private: Tool Extraction from SessionEvent content blocks
|
|
818
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
819
|
+
extractToolsFromContent(event) {
|
|
820
|
+
const content = event.message?.content;
|
|
821
|
+
if (!Array.isArray(content))
|
|
822
|
+
return;
|
|
823
|
+
for (const block of content) {
|
|
824
|
+
if (block.type === 'tool_use') {
|
|
825
|
+
// Assistant content: tool_use block
|
|
826
|
+
const toolUseId = block.id;
|
|
827
|
+
const name = block.name;
|
|
828
|
+
if (!toolUseId || !name)
|
|
829
|
+
continue;
|
|
830
|
+
// Record in toolAnalytics
|
|
831
|
+
const analytics = this.toolAnalytics.get(name) ?? {
|
|
832
|
+
name,
|
|
833
|
+
successCount: 0,
|
|
834
|
+
failureCount: 0,
|
|
835
|
+
totalDuration: 0,
|
|
836
|
+
completedCount: 0,
|
|
837
|
+
pendingCount: 0,
|
|
838
|
+
};
|
|
839
|
+
analytics.pendingCount++;
|
|
840
|
+
this.toolAnalytics.set(name, analytics);
|
|
841
|
+
// Track pending
|
|
842
|
+
this.pendingToolCalls.set(toolUseId, {
|
|
843
|
+
toolUseId,
|
|
844
|
+
name,
|
|
845
|
+
startTime: new Date(event.timestamp),
|
|
846
|
+
});
|
|
847
|
+
this.attributePlanToolCall(name);
|
|
848
|
+
}
|
|
849
|
+
else if (block.type === 'tool_result') {
|
|
850
|
+
// User content: tool_result block
|
|
851
|
+
const toolUseId = block.tool_use_id;
|
|
852
|
+
const isError = block.is_error === true;
|
|
853
|
+
if (!toolUseId)
|
|
854
|
+
continue;
|
|
855
|
+
const pending = this.pendingToolCalls.get(toolUseId);
|
|
856
|
+
if (pending) {
|
|
857
|
+
if (isError) {
|
|
858
|
+
this.recordError(pending.name, categorizeError(block.content), event.timestamp, this.currentModel ?? 'unknown');
|
|
859
|
+
}
|
|
860
|
+
this.pendingToolCalls.delete(toolUseId);
|
|
861
|
+
const analytics = this.toolAnalytics.get(pending.name);
|
|
862
|
+
if (analytics) {
|
|
863
|
+
analytics.pendingCount = Math.max(0, analytics.pendingCount - 1);
|
|
864
|
+
analytics.completedCount++;
|
|
865
|
+
if (isError) {
|
|
866
|
+
analytics.failureCount++;
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
analytics.successCount++;
|
|
870
|
+
}
|
|
871
|
+
// Duration
|
|
872
|
+
const duration = new Date(event.timestamp).getTime() - pending.startTime.getTime();
|
|
873
|
+
if (duration >= 0) {
|
|
874
|
+
analytics.totalDuration += duration;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Truncation detection
|
|
879
|
+
this.detectTruncationInContent(block, event.timestamp);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
detectTruncationInContent(block, timestamp) {
|
|
884
|
+
const content = block.content;
|
|
885
|
+
let text;
|
|
886
|
+
if (typeof content === 'string')
|
|
887
|
+
text = content;
|
|
888
|
+
else if (content)
|
|
889
|
+
text = JSON.stringify(content);
|
|
890
|
+
else
|
|
891
|
+
text = '';
|
|
892
|
+
if (!text)
|
|
893
|
+
return;
|
|
894
|
+
for (const pattern of TRUNCATION_PATTERNS) {
|
|
895
|
+
if (pattern.regex.test(text)) {
|
|
896
|
+
// Try to find the tool name from pending calls
|
|
897
|
+
const toolUseId = block.tool_use_id;
|
|
898
|
+
const pending = toolUseId ? this.pendingToolCalls.get(toolUseId) : undefined;
|
|
899
|
+
this.truncationEvents.push({
|
|
900
|
+
timestamp: new Date(timestamp),
|
|
901
|
+
toolName: pending?.name ?? 'unknown',
|
|
902
|
+
marker: pattern.name,
|
|
903
|
+
});
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
909
|
+
// Private: Tool tracking from FollowEvent
|
|
910
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
911
|
+
recordFollowToolUse(event) {
|
|
912
|
+
const name = event.toolName;
|
|
913
|
+
// Update analytics
|
|
914
|
+
const analytics = this.toolAnalytics.get(name) ?? {
|
|
915
|
+
name,
|
|
916
|
+
successCount: 0,
|
|
917
|
+
failureCount: 0,
|
|
918
|
+
totalDuration: 0,
|
|
919
|
+
completedCount: 0,
|
|
920
|
+
pendingCount: 0,
|
|
921
|
+
};
|
|
922
|
+
analytics.pendingCount++;
|
|
923
|
+
this.toolAnalytics.set(name, analytics);
|
|
924
|
+
// Track pending by tool_use_id from raw
|
|
925
|
+
const raw = event.raw;
|
|
926
|
+
const toolUseId = raw?.id;
|
|
927
|
+
if (toolUseId) {
|
|
928
|
+
this.pendingToolCalls.set(toolUseId, {
|
|
929
|
+
toolUseId,
|
|
930
|
+
name,
|
|
931
|
+
startTime: new Date(event.timestamp),
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
recordFollowToolResult(event) {
|
|
936
|
+
const raw = event.raw;
|
|
937
|
+
if (!raw)
|
|
938
|
+
return;
|
|
939
|
+
const toolUseId = raw.tool_use_id;
|
|
940
|
+
if (!toolUseId)
|
|
941
|
+
return;
|
|
942
|
+
const pending = this.pendingToolCalls.get(toolUseId);
|
|
943
|
+
if (!pending)
|
|
944
|
+
return;
|
|
945
|
+
this.pendingToolCalls.delete(toolUseId);
|
|
946
|
+
const analytics = this.toolAnalytics.get(pending.name);
|
|
947
|
+
if (analytics) {
|
|
948
|
+
analytics.pendingCount = Math.max(0, analytics.pendingCount - 1);
|
|
949
|
+
analytics.completedCount++;
|
|
950
|
+
const isError = raw.is_error === true;
|
|
951
|
+
if (isError) {
|
|
952
|
+
analytics.failureCount++;
|
|
953
|
+
this.recordError(pending.name, categorizeError(raw.content ?? event.summary), event.timestamp, event.model ?? this.currentModel ?? 'unknown');
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
analytics.successCount++;
|
|
957
|
+
}
|
|
958
|
+
const duration = new Date(event.timestamp).getTime() - pending.startTime.getTime();
|
|
959
|
+
if (duration >= 0) {
|
|
960
|
+
analytics.totalDuration += duration;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
recordProviderMetadata(event) {
|
|
965
|
+
const metadata = event.providerMetadata;
|
|
966
|
+
if (!metadata)
|
|
967
|
+
return;
|
|
968
|
+
this.retryAttempts += metadata.retryAttempts?.length ?? 0;
|
|
969
|
+
for (const reason of metadata.finishReasons ?? []) {
|
|
970
|
+
this.finishReasons.set(reason, (this.finishReasons.get(reason) ?? 0) + 1);
|
|
971
|
+
}
|
|
972
|
+
if (metadata.providerError) {
|
|
973
|
+
this.recordError('ProviderAPI', categorizeError(metadata.providerError, metadata.providerError.type), event.timestamp, event.message?.model ?? this.currentModel ?? 'unknown');
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
recordError(tool, category, timestamp, model) {
|
|
977
|
+
const categoryKey = `${tool}\u0000${category}`;
|
|
978
|
+
const current = this.errorByToolCategory.get(categoryKey);
|
|
979
|
+
if (current)
|
|
980
|
+
current.count++;
|
|
981
|
+
else
|
|
982
|
+
this.errorByToolCategory.set(categoryKey, { tool, category, count: 1 });
|
|
983
|
+
const parsed = new Date(timestamp);
|
|
984
|
+
const hour = Number.isNaN(parsed.getTime())
|
|
985
|
+
? timestamp.slice(0, 13)
|
|
986
|
+
: parsed.toISOString().slice(0, 13);
|
|
987
|
+
const bucketKey = `${hour}\u0000${model}\u0000${tool}\u0000${category}`;
|
|
988
|
+
const bucket = this.errorBuckets.get(bucketKey);
|
|
989
|
+
if (bucket)
|
|
990
|
+
bucket.count++;
|
|
991
|
+
else
|
|
992
|
+
this.errorBuckets.set(bucketKey, { hour, model, tool, category, count: 1 });
|
|
993
|
+
}
|
|
994
|
+
detectTruncationFromFollowEvent(event) {
|
|
995
|
+
const raw = event.raw;
|
|
996
|
+
if (!raw)
|
|
997
|
+
return;
|
|
998
|
+
const content = raw.content;
|
|
999
|
+
let text;
|
|
1000
|
+
if (typeof content === 'string')
|
|
1001
|
+
text = content;
|
|
1002
|
+
else if (content)
|
|
1003
|
+
text = JSON.stringify(content);
|
|
1004
|
+
else
|
|
1005
|
+
text = '';
|
|
1006
|
+
if (!text)
|
|
1007
|
+
return;
|
|
1008
|
+
for (const pattern of TRUNCATION_PATTERNS) {
|
|
1009
|
+
if (pattern.regex.test(text)) {
|
|
1010
|
+
const toolUseId = raw.tool_use_id;
|
|
1011
|
+
// Try to find the tool name — it may have been resolved already, so check summary
|
|
1012
|
+
let toolName = 'unknown';
|
|
1013
|
+
if (toolUseId) {
|
|
1014
|
+
const pending = this.pendingToolCalls.get(toolUseId);
|
|
1015
|
+
if (pending)
|
|
1016
|
+
toolName = pending.name;
|
|
1017
|
+
}
|
|
1018
|
+
if (toolName === 'unknown' && event.toolName) {
|
|
1019
|
+
toolName = event.toolName;
|
|
1020
|
+
}
|
|
1021
|
+
this.truncationEvents.push({
|
|
1022
|
+
timestamp: new Date(event.timestamp),
|
|
1023
|
+
toolName,
|
|
1024
|
+
marker: pattern.name,
|
|
1025
|
+
});
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
recordExplicitCompaction(timestamp, reported) {
|
|
1031
|
+
const hasReported = reported !== undefined &&
|
|
1032
|
+
Number.isFinite(reported.tokensBefore) &&
|
|
1033
|
+
Number.isFinite(reported.tokensAfter) &&
|
|
1034
|
+
reported.tokensBefore >= reported.tokensAfter &&
|
|
1035
|
+
reported.tokensAfter >= 0;
|
|
1036
|
+
// Without reported counts, assume the full context was reclaimed until
|
|
1037
|
+
// the next usage event supplies the real post-compaction size.
|
|
1038
|
+
const contextBefore = hasReported ? reported.tokensBefore : this.previousContextSize;
|
|
1039
|
+
const contextAfter = hasReported ? reported.tokensAfter : 0;
|
|
1040
|
+
this.compactionEvents.push({
|
|
1041
|
+
timestamp: new Date(timestamp),
|
|
1042
|
+
contextBefore,
|
|
1043
|
+
contextAfter,
|
|
1044
|
+
tokensReclaimed: Math.max(0, contextBefore - contextAfter),
|
|
1045
|
+
source: hasReported ? 'reported' : 'heuristic',
|
|
1046
|
+
});
|
|
1047
|
+
if (hasReported) {
|
|
1048
|
+
this.previousContextSize = contextAfter;
|
|
1049
|
+
this.currentContextSize = contextAfter;
|
|
1050
|
+
}
|
|
1051
|
+
else {
|
|
1052
|
+
this.pendingHeuristicCompactionIndex = this.compactionEvents.length - 1;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Fills a pending heuristic compaction with the first observed
|
|
1057
|
+
* post-compaction context size. Returns true when the observed drop was
|
|
1058
|
+
* consumed by that pending event, so the caller must not record it again.
|
|
1059
|
+
*/
|
|
1060
|
+
resolvePendingHeuristicCompaction(contextSize) {
|
|
1061
|
+
if (this.pendingHeuristicCompactionIndex === null)
|
|
1062
|
+
return false;
|
|
1063
|
+
const compaction = this.compactionEvents[this.pendingHeuristicCompactionIndex];
|
|
1064
|
+
this.pendingHeuristicCompactionIndex = null;
|
|
1065
|
+
if (!compaction)
|
|
1066
|
+
return false;
|
|
1067
|
+
if (contextSize < compaction.contextBefore) {
|
|
1068
|
+
compaction.contextAfter = contextSize;
|
|
1069
|
+
compaction.tokensReclaimed = compaction.contextBefore - contextSize;
|
|
1070
|
+
}
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1074
|
+
// Private: Task State from SessionEvent
|
|
1075
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1076
|
+
extractTaskStateFromEvent(event) {
|
|
1077
|
+
const content = event.message?.content;
|
|
1078
|
+
if (!Array.isArray(content))
|
|
1079
|
+
return;
|
|
1080
|
+
for (const block of content) {
|
|
1081
|
+
if (block.type === 'tool_use') {
|
|
1082
|
+
const name = block.name;
|
|
1083
|
+
const input = block.input;
|
|
1084
|
+
const toolUseId = block.id;
|
|
1085
|
+
if (!input)
|
|
1086
|
+
continue;
|
|
1087
|
+
if (name === 'TaskCreate' && toolUseId) {
|
|
1088
|
+
this.pendingTaskCreates.set(toolUseId, {
|
|
1089
|
+
subject: input.subject || 'Untitled',
|
|
1090
|
+
description: input.description,
|
|
1091
|
+
activeForm: input.activeForm,
|
|
1092
|
+
subagentType: input.subagentType,
|
|
1093
|
+
isGoalGate: input.isGoalGate,
|
|
1094
|
+
timestamp: new Date(event.timestamp),
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
else if (name === 'TaskUpdate') {
|
|
1098
|
+
this.applyTaskUpdate(input);
|
|
1099
|
+
this.syncPlanStepFromTaskUpdate(input, new Date(event.timestamp));
|
|
1100
|
+
}
|
|
1101
|
+
else if (name === 'TodoWrite') {
|
|
1102
|
+
this.handleTodoWrite(input, new Date(event.timestamp));
|
|
1103
|
+
}
|
|
1104
|
+
else if (name === 'UpdatePlan') {
|
|
1105
|
+
this.handleUpdatePlan(input, new Date(event.timestamp));
|
|
1106
|
+
}
|
|
1107
|
+
else if ((name === 'Agent' || name === 'Task') && toolUseId) {
|
|
1108
|
+
this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
else if (block.type === 'tool_result') {
|
|
1112
|
+
const toolUseId = block.tool_use_id;
|
|
1113
|
+
if (!toolUseId)
|
|
1114
|
+
continue;
|
|
1115
|
+
const isError = block.is_error ?? false;
|
|
1116
|
+
this.resolveTaskCreate(toolUseId, block.content ?? block.output, event.timestamp, isError);
|
|
1117
|
+
this.resolveAgentResult(toolUseId, isError, event.timestamp);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1122
|
+
// Private: Task State from FollowEvent
|
|
1123
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1124
|
+
extractTaskStateFromFollowEvent(event) {
|
|
1125
|
+
const raw = event.raw;
|
|
1126
|
+
if (event.type === 'tool_use' && event.toolName) {
|
|
1127
|
+
if (!raw?.input)
|
|
1128
|
+
return;
|
|
1129
|
+
const input = raw.input;
|
|
1130
|
+
const toolUseId = raw.id;
|
|
1131
|
+
if (event.toolName === 'TaskCreate' && toolUseId) {
|
|
1132
|
+
this.pendingTaskCreates.set(toolUseId, {
|
|
1133
|
+
subject: input.subject || 'Untitled',
|
|
1134
|
+
description: input.description,
|
|
1135
|
+
activeForm: input.activeForm,
|
|
1136
|
+
subagentType: input.subagentType,
|
|
1137
|
+
isGoalGate: input.isGoalGate,
|
|
1138
|
+
timestamp: new Date(event.timestamp),
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
else if (event.toolName === 'TaskUpdate') {
|
|
1142
|
+
this.applyTaskUpdate(input);
|
|
1143
|
+
this.syncPlanStepFromTaskUpdate(input, new Date(event.timestamp));
|
|
1144
|
+
}
|
|
1145
|
+
else if (event.toolName === 'TodoWrite') {
|
|
1146
|
+
this.handleTodoWrite(input, new Date(event.timestamp));
|
|
1147
|
+
}
|
|
1148
|
+
else if (event.toolName === 'UpdatePlan') {
|
|
1149
|
+
this.handleUpdatePlan(input, new Date(event.timestamp));
|
|
1150
|
+
}
|
|
1151
|
+
else if ((event.toolName === 'Agent' || event.toolName === 'Task') && toolUseId) {
|
|
1152
|
+
this.handleAgentSpawn(toolUseId, input, new Date(event.timestamp));
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
else if (event.type === 'tool_result') {
|
|
1156
|
+
if (!raw)
|
|
1157
|
+
return;
|
|
1158
|
+
const toolUseId = raw.tool_use_id;
|
|
1159
|
+
if (!toolUseId)
|
|
1160
|
+
return;
|
|
1161
|
+
const isError = raw.is_error ?? false;
|
|
1162
|
+
this.resolveTaskCreate(toolUseId, raw.content, event.timestamp, isError);
|
|
1163
|
+
this.resolveAgentResult(toolUseId, isError, event.timestamp);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
applyTaskUpdate(input) {
|
|
1167
|
+
const taskId = input.taskId;
|
|
1168
|
+
if (!taskId)
|
|
1169
|
+
return;
|
|
1170
|
+
const existing = this.tasks.get(taskId);
|
|
1171
|
+
if (existing) {
|
|
1172
|
+
if (input.status) {
|
|
1173
|
+
const newStatus = input.status;
|
|
1174
|
+
if (newStatus === 'deleted') {
|
|
1175
|
+
this.tasks.delete(taskId);
|
|
1176
|
+
if (this.activeTaskId === taskId)
|
|
1177
|
+
this.activeTaskId = null;
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const oldStatus = existing.status;
|
|
1181
|
+
existing.status = newStatus;
|
|
1182
|
+
existing.updatedAt = new Date();
|
|
1183
|
+
if (newStatus === 'in_progress' && oldStatus !== 'in_progress') {
|
|
1184
|
+
this.activeTaskId = taskId;
|
|
1185
|
+
}
|
|
1186
|
+
else if (oldStatus === 'in_progress' && newStatus !== 'in_progress') {
|
|
1187
|
+
if (this.activeTaskId === taskId)
|
|
1188
|
+
this.activeTaskId = null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (input.subject)
|
|
1192
|
+
existing.subject = input.subject;
|
|
1193
|
+
if (input.description)
|
|
1194
|
+
existing.description = input.description;
|
|
1195
|
+
if (input.activeForm)
|
|
1196
|
+
existing.activeForm = input.activeForm;
|
|
1197
|
+
if (input.addBlockedBy && Array.isArray(input.addBlockedBy)) {
|
|
1198
|
+
for (const id of input.addBlockedBy) {
|
|
1199
|
+
const idStr = String(id);
|
|
1200
|
+
if (!existing.blockedBy.includes(idStr)) {
|
|
1201
|
+
existing.blockedBy.push(idStr);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (input.addBlocks && Array.isArray(input.addBlocks)) {
|
|
1206
|
+
for (const id of input.addBlocks) {
|
|
1207
|
+
const idStr = String(id);
|
|
1208
|
+
if (!existing.blocks.includes(idStr)) {
|
|
1209
|
+
existing.blocks.push(idStr);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
// Re-evaluate goal gate after update
|
|
1214
|
+
existing.isGoalGate = this.isGoalGateTask(existing);
|
|
1215
|
+
}
|
|
1216
|
+
else {
|
|
1217
|
+
// TaskUpdate for unknown task — create placeholder
|
|
1218
|
+
const status = input.status || 'pending';
|
|
1219
|
+
if (status === 'deleted')
|
|
1220
|
+
return;
|
|
1221
|
+
const now = new Date();
|
|
1222
|
+
const task = {
|
|
1223
|
+
taskId,
|
|
1224
|
+
subject: input.subject || `Task ${taskId}`,
|
|
1225
|
+
description: input.description,
|
|
1226
|
+
status: status,
|
|
1227
|
+
createdAt: now,
|
|
1228
|
+
updatedAt: now,
|
|
1229
|
+
blockedBy: [],
|
|
1230
|
+
blocks: [],
|
|
1231
|
+
associatedToolCalls: [],
|
|
1232
|
+
activeForm: input.activeForm,
|
|
1233
|
+
};
|
|
1234
|
+
task.isGoalGate = this.isGoalGateTask(task);
|
|
1235
|
+
this.tasks.set(taskId, task);
|
|
1236
|
+
if (status === 'in_progress') {
|
|
1237
|
+
this.activeTaskId = taskId;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
syncPlanStepFromTaskUpdate(input, now) {
|
|
1242
|
+
if (!this.planState || !input.status)
|
|
1243
|
+
return;
|
|
1244
|
+
const taskId = String(input.taskId ?? input.task_id ?? '');
|
|
1245
|
+
const stepId = taskId.startsWith('plan-') ? `step-${taskId.slice(5)}` : taskId;
|
|
1246
|
+
const step = this.planState.steps.find((candidate) => candidate.id === stepId);
|
|
1247
|
+
if (!step)
|
|
1248
|
+
return;
|
|
1249
|
+
const rawStatus = String(input.status).toLowerCase();
|
|
1250
|
+
const status = rawStatus === 'completed'
|
|
1251
|
+
? 'completed'
|
|
1252
|
+
: rawStatus === 'in_progress' || rawStatus === 'in-progress'
|
|
1253
|
+
? 'in_progress'
|
|
1254
|
+
: rawStatus === 'deleted'
|
|
1255
|
+
? 'skipped'
|
|
1256
|
+
: 'pending';
|
|
1257
|
+
if (status === 'in_progress' && step.status !== 'in_progress') {
|
|
1258
|
+
step.startedAt = now.toISOString();
|
|
1259
|
+
}
|
|
1260
|
+
if (status === 'completed' && step.status !== status) {
|
|
1261
|
+
step.startedAt ??= now.toISOString();
|
|
1262
|
+
step.completedAt = now.toISOString();
|
|
1263
|
+
step.durationMs = Math.max(0, now.getTime() - new Date(step.startedAt).getTime());
|
|
1264
|
+
}
|
|
1265
|
+
step.status = status;
|
|
1266
|
+
const completed = this.planState.steps.filter((candidate) => candidate.status === 'completed');
|
|
1267
|
+
this.planState.completionRate =
|
|
1268
|
+
this.planState.steps.length > 0 ? completed.length / this.planState.steps.length : 0;
|
|
1269
|
+
this.planState.active = this.planState.steps.some((candidate) => candidate.status === 'pending' || candidate.status === 'in_progress');
|
|
1270
|
+
}
|
|
1271
|
+
resolveTaskCreate(toolUseId, content, timestamp, isError) {
|
|
1272
|
+
const pending = this.pendingTaskCreates.get(toolUseId);
|
|
1273
|
+
if (!pending)
|
|
1274
|
+
return;
|
|
1275
|
+
this.pendingTaskCreates.delete(toolUseId);
|
|
1276
|
+
// Don't create task on error
|
|
1277
|
+
if (isError)
|
|
1278
|
+
return;
|
|
1279
|
+
const taskId = this.extractTaskIdFromResult(content);
|
|
1280
|
+
if (!taskId)
|
|
1281
|
+
return;
|
|
1282
|
+
const now = new Date(timestamp);
|
|
1283
|
+
const task = {
|
|
1284
|
+
taskId,
|
|
1285
|
+
subject: pending.subject,
|
|
1286
|
+
description: pending.description,
|
|
1287
|
+
status: 'pending',
|
|
1288
|
+
createdAt: pending.timestamp ?? now,
|
|
1289
|
+
updatedAt: now,
|
|
1290
|
+
blockedBy: [],
|
|
1291
|
+
blocks: [],
|
|
1292
|
+
associatedToolCalls: [],
|
|
1293
|
+
activeForm: pending.activeForm,
|
|
1294
|
+
subagentType: pending.subagentType,
|
|
1295
|
+
isGoalGate: pending.isGoalGate,
|
|
1296
|
+
};
|
|
1297
|
+
// Evaluate goal gate from content if not explicitly set
|
|
1298
|
+
task.isGoalGate = task.isGoalGate || this.isGoalGateTask(task);
|
|
1299
|
+
this.tasks.set(taskId, task);
|
|
1300
|
+
}
|
|
1301
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1302
|
+
// Private: TodoWrite handling (OpenCode)
|
|
1303
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1304
|
+
handleTodoWrite(input, now) {
|
|
1305
|
+
const todos = input.todos;
|
|
1306
|
+
if (!Array.isArray(todos))
|
|
1307
|
+
return;
|
|
1308
|
+
// Remove all non-subagent tasks (subagent tasks should persist)
|
|
1309
|
+
for (const [taskId, task] of this.tasks) {
|
|
1310
|
+
if (!task.isSubagent) {
|
|
1311
|
+
this.tasks.delete(taskId);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
// Reset active task if it was a non-subagent task
|
|
1315
|
+
if (this.activeTaskId && !this.tasks.has(this.activeTaskId)) {
|
|
1316
|
+
this.activeTaskId = null;
|
|
1317
|
+
}
|
|
1318
|
+
for (let i = 0; i < todos.length; i++) {
|
|
1319
|
+
const todo = todos[i];
|
|
1320
|
+
const taskId = `todo-${i}`;
|
|
1321
|
+
const content = String(todo.content || todo.subject || `Todo ${i + 1}`);
|
|
1322
|
+
const rawStatus = String(todo.status || 'pending').toLowerCase();
|
|
1323
|
+
const priority = todo.priority ? String(todo.priority) : undefined;
|
|
1324
|
+
// Map OpenCode todo status values to TaskStatus
|
|
1325
|
+
let status;
|
|
1326
|
+
if (rawStatus === 'completed' || rawStatus === 'done') {
|
|
1327
|
+
status = 'completed';
|
|
1328
|
+
}
|
|
1329
|
+
else if (rawStatus === 'in_progress' || rawStatus === 'in-progress') {
|
|
1330
|
+
status = 'in_progress';
|
|
1331
|
+
}
|
|
1332
|
+
else {
|
|
1333
|
+
status = 'pending';
|
|
1334
|
+
}
|
|
1335
|
+
const task = {
|
|
1336
|
+
taskId,
|
|
1337
|
+
subject: content,
|
|
1338
|
+
description: priority ? `Priority: ${priority}` : undefined,
|
|
1339
|
+
status,
|
|
1340
|
+
createdAt: now,
|
|
1341
|
+
updatedAt: now,
|
|
1342
|
+
blockedBy: [],
|
|
1343
|
+
blocks: [],
|
|
1344
|
+
associatedToolCalls: [],
|
|
1345
|
+
};
|
|
1346
|
+
this.tasks.set(taskId, task);
|
|
1347
|
+
if (status === 'in_progress') {
|
|
1348
|
+
this.activeTaskId = taskId;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
// Second pass: parse dependency info from content text
|
|
1352
|
+
for (let i = 0; i < todos.length; i++) {
|
|
1353
|
+
const task = this.tasks.get(`todo-${i}`);
|
|
1354
|
+
if (!task)
|
|
1355
|
+
continue;
|
|
1356
|
+
// Check for explicit blockedBy field
|
|
1357
|
+
if (Array.isArray(todos[i].blockedBy)) {
|
|
1358
|
+
for (const ref of todos[i].blockedBy) {
|
|
1359
|
+
const refStr = String(ref);
|
|
1360
|
+
if (!task.blockedBy.includes(refStr)) {
|
|
1361
|
+
task.blockedBy.push(refStr);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
// Parse dependency references from content text
|
|
1366
|
+
const deps = parseTodoDependencies(String(todos[i].content || ''), todos);
|
|
1367
|
+
for (const depId of deps) {
|
|
1368
|
+
if (depId !== `todo-${i}` && !task.blockedBy.includes(depId)) {
|
|
1369
|
+
task.blockedBy.push(depId);
|
|
1370
|
+
const depTask = this.tasks.get(depId);
|
|
1371
|
+
if (depTask && !depTask.blocks.includes(`todo-${i}`)) {
|
|
1372
|
+
depTask.blocks.push(`todo-${i}`);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1379
|
+
// Private: UpdatePlan handling (Codex)
|
|
1380
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1381
|
+
handleUpdatePlan(input, now) {
|
|
1382
|
+
const plan = input.plan;
|
|
1383
|
+
if (!Array.isArray(plan))
|
|
1384
|
+
return;
|
|
1385
|
+
const seenPlanTaskIds = new Set();
|
|
1386
|
+
let activePlanTaskId = null;
|
|
1387
|
+
for (let i = 0; i < plan.length; i++) {
|
|
1388
|
+
const entry = plan[i];
|
|
1389
|
+
const step = String(entry.step || '').trim();
|
|
1390
|
+
if (!step)
|
|
1391
|
+
continue;
|
|
1392
|
+
const rawStatus = String(entry.status || 'pending').toLowerCase();
|
|
1393
|
+
let status;
|
|
1394
|
+
if (rawStatus === 'completed')
|
|
1395
|
+
status = 'completed';
|
|
1396
|
+
else if (rawStatus === 'in_progress' || rawStatus === 'in-progress')
|
|
1397
|
+
status = 'in_progress';
|
|
1398
|
+
else
|
|
1399
|
+
status = 'pending';
|
|
1400
|
+
const taskId = `plan-${i}`;
|
|
1401
|
+
seenPlanTaskIds.add(taskId);
|
|
1402
|
+
const existing = this.tasks.get(taskId);
|
|
1403
|
+
if (existing) {
|
|
1404
|
+
existing.subject = step;
|
|
1405
|
+
existing.status = status;
|
|
1406
|
+
existing.updatedAt = now;
|
|
1407
|
+
if (!existing.activeForm) {
|
|
1408
|
+
existing.activeForm = `Working on ${step}`;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
const task = {
|
|
1413
|
+
taskId,
|
|
1414
|
+
subject: step,
|
|
1415
|
+
status,
|
|
1416
|
+
createdAt: now,
|
|
1417
|
+
updatedAt: now,
|
|
1418
|
+
activeForm: `Working on ${step}`,
|
|
1419
|
+
blockedBy: [],
|
|
1420
|
+
blocks: [],
|
|
1421
|
+
associatedToolCalls: [],
|
|
1422
|
+
};
|
|
1423
|
+
this.tasks.set(taskId, task);
|
|
1424
|
+
}
|
|
1425
|
+
if (status === 'in_progress') {
|
|
1426
|
+
activePlanTaskId = taskId;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
// Mark missing synthetic plan tasks as deleted
|
|
1430
|
+
for (const [taskId, task] of this.tasks) {
|
|
1431
|
+
if (!taskId.startsWith('plan-'))
|
|
1432
|
+
continue;
|
|
1433
|
+
if (!seenPlanTaskIds.has(taskId) && task.status !== 'deleted') {
|
|
1434
|
+
task.status = 'deleted';
|
|
1435
|
+
task.updatedAt = now;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
if (activePlanTaskId) {
|
|
1439
|
+
this.activeTaskId = activePlanTaskId;
|
|
1440
|
+
}
|
|
1441
|
+
else if (this.activeTaskId?.startsWith('plan-')) {
|
|
1442
|
+
this.activeTaskId = null;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1446
|
+
// Private: Agent/Task (subagent) as tracked task
|
|
1447
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1448
|
+
handleAgentSpawn(toolUseId, input, now) {
|
|
1449
|
+
const agentTaskId = `agent-${toolUseId}`;
|
|
1450
|
+
const description = input.description ? String(input.description) : 'Subagent';
|
|
1451
|
+
const subagentType = input.subagent_type || input.subagentType || undefined;
|
|
1452
|
+
const task = {
|
|
1453
|
+
taskId: agentTaskId,
|
|
1454
|
+
subject: description,
|
|
1455
|
+
status: 'in_progress',
|
|
1456
|
+
createdAt: now,
|
|
1457
|
+
updatedAt: now,
|
|
1458
|
+
activeForm: subagentType ? `Running ${subagentType} agent` : 'Running subagent',
|
|
1459
|
+
blockedBy: [],
|
|
1460
|
+
blocks: [],
|
|
1461
|
+
associatedToolCalls: [],
|
|
1462
|
+
isSubagent: true,
|
|
1463
|
+
subagentType,
|
|
1464
|
+
toolUseId,
|
|
1465
|
+
};
|
|
1466
|
+
this.tasks.set(agentTaskId, task);
|
|
1467
|
+
}
|
|
1468
|
+
resolveAgentResult(toolUseId, isError, timestamp) {
|
|
1469
|
+
const agentTaskId = `agent-${toolUseId}`;
|
|
1470
|
+
const agentTask = this.tasks.get(agentTaskId);
|
|
1471
|
+
if (!agentTask)
|
|
1472
|
+
return;
|
|
1473
|
+
agentTask.status = isError ? 'deleted' : 'completed';
|
|
1474
|
+
agentTask.updatedAt = new Date(timestamp);
|
|
1475
|
+
}
|
|
1476
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1477
|
+
// Private: Goal gate detection
|
|
1478
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1479
|
+
isGoalGateTask(task) {
|
|
1480
|
+
if (GOAL_GATE_REGEX.test(task.subject))
|
|
1481
|
+
return true;
|
|
1482
|
+
if (task.description && GOAL_GATE_REGEX.test(task.description))
|
|
1483
|
+
return true;
|
|
1484
|
+
if (task.blocks.length >= 3)
|
|
1485
|
+
return true;
|
|
1486
|
+
return false;
|
|
1487
|
+
}
|
|
1488
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1489
|
+
// Private: Subagent Tracking from SessionEvent
|
|
1490
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1491
|
+
extractSubagentFromEvent(event) {
|
|
1492
|
+
const content = event.message?.content;
|
|
1493
|
+
if (!Array.isArray(content))
|
|
1494
|
+
return;
|
|
1495
|
+
for (const block of content) {
|
|
1496
|
+
if (block.type === 'tool_use' && block.name === 'Task') {
|
|
1497
|
+
const input = block.input;
|
|
1498
|
+
const toolUseId = block.id;
|
|
1499
|
+
if (!input || !toolUseId)
|
|
1500
|
+
continue;
|
|
1501
|
+
const info = {
|
|
1502
|
+
id: toolUseId,
|
|
1503
|
+
description: input.description || 'Unknown task',
|
|
1504
|
+
subagentType: input.subagent_type || input.subagentType || 'general',
|
|
1505
|
+
spawnTime: event.timestamp,
|
|
1506
|
+
status: 'running',
|
|
1507
|
+
};
|
|
1508
|
+
const idx = this.subagents.length;
|
|
1509
|
+
this.subagents.push(info);
|
|
1510
|
+
this.pendingSubagents.set(toolUseId, idx);
|
|
1511
|
+
}
|
|
1512
|
+
else if (block.type === 'tool_result') {
|
|
1513
|
+
const toolUseId = block.tool_use_id;
|
|
1514
|
+
if (!toolUseId)
|
|
1515
|
+
continue;
|
|
1516
|
+
this.completeSubagent(toolUseId, event.timestamp);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1521
|
+
// Private: Subagent Tracking from FollowEvent
|
|
1522
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1523
|
+
extractSubagentFromFollowEvent(event) {
|
|
1524
|
+
const raw = event.raw;
|
|
1525
|
+
if (event.type === 'tool_use' && event.toolName === 'Task') {
|
|
1526
|
+
if (!raw?.input)
|
|
1527
|
+
return;
|
|
1528
|
+
const input = raw.input;
|
|
1529
|
+
const toolUseId = raw.id || '';
|
|
1530
|
+
const info = {
|
|
1531
|
+
id: toolUseId,
|
|
1532
|
+
description: input.description || 'Unknown task',
|
|
1533
|
+
subagentType: input.subagent_type || input.subagentType || 'general',
|
|
1534
|
+
spawnTime: event.timestamp,
|
|
1535
|
+
status: 'running',
|
|
1536
|
+
};
|
|
1537
|
+
const idx = this.subagents.length;
|
|
1538
|
+
this.subagents.push(info);
|
|
1539
|
+
if (toolUseId) {
|
|
1540
|
+
this.pendingSubagents.set(toolUseId, idx);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
else if (event.type === 'tool_result') {
|
|
1544
|
+
if (!raw)
|
|
1545
|
+
return;
|
|
1546
|
+
const toolUseId = raw.tool_use_id;
|
|
1547
|
+
if (!toolUseId)
|
|
1548
|
+
return;
|
|
1549
|
+
this.completeSubagent(toolUseId, event.timestamp);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
completeSubagent(toolUseId, timestamp) {
|
|
1553
|
+
const idx = this.pendingSubagents.get(toolUseId);
|
|
1554
|
+
if (idx === undefined)
|
|
1555
|
+
return;
|
|
1556
|
+
this.pendingSubagents.delete(toolUseId);
|
|
1557
|
+
const agent = this.subagents[idx];
|
|
1558
|
+
agent.status = 'completed';
|
|
1559
|
+
agent.completionTime = timestamp;
|
|
1560
|
+
const start = new Date(agent.spawnTime).getTime();
|
|
1561
|
+
const end = new Date(timestamp).getTime();
|
|
1562
|
+
if (!isNaN(start) && !isNaN(end) && end >= start) {
|
|
1563
|
+
agent.durationMs = end - start;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1567
|
+
// Private: Plan Extraction from SessionEvent
|
|
1568
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1569
|
+
extractPlanFromSessionEvent(event) {
|
|
1570
|
+
// Use toFollowEvents to properly split assistant messages into
|
|
1571
|
+
// per-content-block FollowEvents (one per tool_use + one for text).
|
|
1572
|
+
// This ensures PlanExtractor sees type:'tool_use' for plan tools
|
|
1573
|
+
// like EnterPlanMode, with raw.input set correctly.
|
|
1574
|
+
const providerId = this._providerId ?? 'claude-code';
|
|
1575
|
+
const followEvents = toFollowEvents(event, providerId);
|
|
1576
|
+
for (const fe of followEvents) {
|
|
1577
|
+
this.processPlanFollowEvent(fe);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
processPlanFollowEvent(event) {
|
|
1581
|
+
const now = new Date(event.timestamp);
|
|
1582
|
+
if (event.type === 'tool_use' && event.toolName === 'EnterPlanMode') {
|
|
1583
|
+
this.planEnteredAt = now;
|
|
1584
|
+
this.planState = {
|
|
1585
|
+
active: true,
|
|
1586
|
+
steps: [],
|
|
1587
|
+
enteredAt: now,
|
|
1588
|
+
source: event.providerId,
|
|
1589
|
+
};
|
|
1590
|
+
}
|
|
1591
|
+
const updated = this.planExtractor.processEvent(event);
|
|
1592
|
+
if (updated && this.planExtractor.plan) {
|
|
1593
|
+
this.planState = this.convertExtractedPlanToPlanState(this.planExtractor.plan, now);
|
|
1594
|
+
}
|
|
1595
|
+
if (event.type === 'tool_use' && event.toolName === 'ExitPlanMode' && this.planState) {
|
|
1596
|
+
this.planState.exitedAt = now;
|
|
1597
|
+
if (this.planState.enteredAt) {
|
|
1598
|
+
this.planState.totalDurationMs = now.getTime() - this.planState.enteredAt.getTime();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
convertExtractedPlanToPlanState(extracted, now) {
|
|
1603
|
+
const previous = this.planState;
|
|
1604
|
+
const previousSteps = new Map((previous?.steps ?? []).map((step) => [`${step.id}\u0000${step.description}`, step]));
|
|
1605
|
+
const completed = extracted.steps.filter((s) => s.status === 'completed').length;
|
|
1606
|
+
const total = extracted.steps.length;
|
|
1607
|
+
const steps = extracted.steps.map((step) => {
|
|
1608
|
+
const old = previousSteps.get(`${step.id}\u0000${step.description}`);
|
|
1609
|
+
const merged = { ...step, ...old, status: step.status };
|
|
1610
|
+
if (step.status === 'in_progress' && old?.status !== 'in_progress' && !merged.startedAt) {
|
|
1611
|
+
merged.startedAt = now.toISOString();
|
|
1612
|
+
}
|
|
1613
|
+
if ((step.status === 'completed' || step.status === 'failed') &&
|
|
1614
|
+
old?.status !== step.status &&
|
|
1615
|
+
!merged.completedAt) {
|
|
1616
|
+
merged.startedAt ??= previous?.enteredAt?.toISOString() ?? now.toISOString();
|
|
1617
|
+
merged.completedAt = now.toISOString();
|
|
1618
|
+
merged.durationMs = Math.max(0, now.getTime() - new Date(merged.startedAt).getTime());
|
|
1619
|
+
}
|
|
1620
|
+
return merged;
|
|
1621
|
+
});
|
|
1622
|
+
return {
|
|
1623
|
+
active: steps.some((step) => step.status === 'pending' || step.status === 'in_progress'),
|
|
1624
|
+
steps,
|
|
1625
|
+
title: extracted.title,
|
|
1626
|
+
source: extracted.source,
|
|
1627
|
+
completionRate: total > 0 ? completed / total : 0,
|
|
1628
|
+
rawMarkdown: extracted.rawMarkdown,
|
|
1629
|
+
enteredAt: previous?.enteredAt ?? this.planEnteredAt ?? now,
|
|
1630
|
+
exitedAt: previous?.exitedAt,
|
|
1631
|
+
totalDurationMs: previous?.totalDurationMs,
|
|
1632
|
+
prompt: previous?.prompt,
|
|
1633
|
+
revision: previous?.revision,
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
getActivePlanStep() {
|
|
1637
|
+
return this.planState?.steps.find((step) => step.status === 'in_progress');
|
|
1638
|
+
}
|
|
1639
|
+
attributePlanUsage(tokens, costUsd) {
|
|
1640
|
+
const step = this.getActivePlanStep();
|
|
1641
|
+
if (!step)
|
|
1642
|
+
return;
|
|
1643
|
+
step.tokensUsed = (step.tokensUsed ?? 0) + tokens;
|
|
1644
|
+
if (costUsd > 0)
|
|
1645
|
+
step.costUsd = (step.costUsd ?? 0) + costUsd;
|
|
1646
|
+
}
|
|
1647
|
+
attributePlanToolCall(toolName) {
|
|
1648
|
+
if (['EnterPlanMode', 'ExitPlanMode', 'UpdatePlan', 'TaskUpdate'].includes(toolName))
|
|
1649
|
+
return;
|
|
1650
|
+
const step = this.getActivePlanStep();
|
|
1651
|
+
if (step)
|
|
1652
|
+
step.toolCalls = (step.toolCalls ?? 0) + 1;
|
|
1653
|
+
}
|
|
1654
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1655
|
+
// Private: Context Attribution from SessionEvent
|
|
1656
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1657
|
+
attributeContextFromEvent(event) {
|
|
1658
|
+
const content = event.message?.content;
|
|
1659
|
+
if (event.type === 'user') {
|
|
1660
|
+
if (Array.isArray(content)) {
|
|
1661
|
+
for (const block of content) {
|
|
1662
|
+
if (block.type === 'tool_result') {
|
|
1663
|
+
const text = typeof block.content === 'string'
|
|
1664
|
+
? block.content
|
|
1665
|
+
: JSON.stringify(block.content || '');
|
|
1666
|
+
this.contextAttribution.toolOutputs += this.estimateTokens(text);
|
|
1667
|
+
}
|
|
1668
|
+
else if (block.type === 'text') {
|
|
1669
|
+
const text = block.text || '';
|
|
1670
|
+
if (this.isSystemPromptContent(text)) {
|
|
1671
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(text);
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
this.contextAttribution.userMessages += this.estimateTokens(text);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
else if (typeof content === 'string') {
|
|
1680
|
+
if (this.isSystemPromptContent(content)) {
|
|
1681
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(content);
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
this.contextAttribution.userMessages += this.estimateTokens(content);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
else if (event.type === 'assistant') {
|
|
1689
|
+
if (Array.isArray(content)) {
|
|
1690
|
+
for (const block of content) {
|
|
1691
|
+
if (block.type === 'thinking') {
|
|
1692
|
+
this.contextAttribution.thinking += this.estimateTokens(block.thinking || '');
|
|
1693
|
+
}
|
|
1694
|
+
else if (block.type === 'tool_use') {
|
|
1695
|
+
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input || '');
|
|
1696
|
+
this.contextAttribution.toolInputs += this.estimateTokens(input);
|
|
1697
|
+
}
|
|
1698
|
+
else if (block.type === 'text') {
|
|
1699
|
+
this.contextAttribution.assistantResponses += this.estimateTokens(block.text || '');
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
else if (event.type === 'summary') {
|
|
1705
|
+
const text = this.extractTextContent(event) || '';
|
|
1706
|
+
if (text) {
|
|
1707
|
+
this.contextAttribution.other += this.estimateTokens(text);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
else if (event.type === 'system') {
|
|
1711
|
+
const text = this.extractTextContent(event) || '';
|
|
1712
|
+
if (text) {
|
|
1713
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(text);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1718
|
+
// Private: Context Attribution from FollowEvent
|
|
1719
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1720
|
+
attributeContextFromFollowEvent(event) {
|
|
1721
|
+
const raw = event.raw;
|
|
1722
|
+
// Try to get full content blocks from raw.message.content
|
|
1723
|
+
const message = raw?.message;
|
|
1724
|
+
const content = message?.content;
|
|
1725
|
+
if (event.type === 'user') {
|
|
1726
|
+
if (Array.isArray(content)) {
|
|
1727
|
+
for (const block of content) {
|
|
1728
|
+
if (block.type === 'tool_result') {
|
|
1729
|
+
const text = typeof block.content === 'string'
|
|
1730
|
+
? block.content
|
|
1731
|
+
: JSON.stringify(block.content || '');
|
|
1732
|
+
this.contextAttribution.toolOutputs += this.estimateTokens(text);
|
|
1733
|
+
}
|
|
1734
|
+
else if (block.type === 'text') {
|
|
1735
|
+
const text = block.text || '';
|
|
1736
|
+
if (this.isSystemPromptContent(text)) {
|
|
1737
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(text);
|
|
1738
|
+
}
|
|
1739
|
+
else {
|
|
1740
|
+
this.contextAttribution.userMessages += this.estimateTokens(text);
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
else if (event.summary) {
|
|
1746
|
+
if (this.isSystemPromptContent(event.summary)) {
|
|
1747
|
+
this.contextAttribution.systemPrompt += this.estimateTokens(event.summary);
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
this.contextAttribution.userMessages += this.estimateTokens(event.summary);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
else if (event.type === 'assistant') {
|
|
1755
|
+
if (Array.isArray(content)) {
|
|
1756
|
+
for (const block of content) {
|
|
1757
|
+
if (block.type === 'thinking') {
|
|
1758
|
+
this.contextAttribution.thinking += this.estimateTokens(block.thinking || '');
|
|
1759
|
+
}
|
|
1760
|
+
else if (block.type === 'tool_use') {
|
|
1761
|
+
const input = typeof block.input === 'string' ? block.input : JSON.stringify(block.input || '');
|
|
1762
|
+
this.contextAttribution.toolInputs += this.estimateTokens(input);
|
|
1763
|
+
}
|
|
1764
|
+
else if (block.type === 'text') {
|
|
1765
|
+
this.contextAttribution.assistantResponses += this.estimateTokens(block.text || '');
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
else if (event.summary) {
|
|
1770
|
+
this.contextAttribution.assistantResponses += this.estimateTokens(event.summary);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
else if (event.type === 'tool_use') {
|
|
1774
|
+
// Try raw input first (summary is truncated to ~80 chars)
|
|
1775
|
+
const rawInput = raw?.input != null ? JSON.stringify(raw.input) : null;
|
|
1776
|
+
const text = rawInput || event.summary || '';
|
|
1777
|
+
if (text)
|
|
1778
|
+
this.contextAttribution.toolInputs += this.estimateTokens(text);
|
|
1779
|
+
}
|
|
1780
|
+
else if (event.type === 'tool_result') {
|
|
1781
|
+
const rawContent = raw?.content;
|
|
1782
|
+
const text = typeof rawContent === 'string'
|
|
1783
|
+
? rawContent
|
|
1784
|
+
: rawContent
|
|
1785
|
+
? JSON.stringify(rawContent)
|
|
1786
|
+
: event.summary || '';
|
|
1787
|
+
if (text)
|
|
1788
|
+
this.contextAttribution.toolOutputs += this.estimateTokens(text);
|
|
1789
|
+
}
|
|
1790
|
+
else if (event.type === 'summary') {
|
|
1791
|
+
if (event.summary) {
|
|
1792
|
+
this.contextAttribution.other += this.estimateTokens(event.summary);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1797
|
+
// Private: Permission Mode
|
|
1798
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1799
|
+
trackPermissionMode(event) {
|
|
1800
|
+
if (!event.permissionMode)
|
|
1801
|
+
return;
|
|
1802
|
+
const mode = event.permissionMode;
|
|
1803
|
+
if (mode !== this.currentPermissionMode) {
|
|
1804
|
+
this.permissionModeHistory.push({
|
|
1805
|
+
timestamp: event.timestamp,
|
|
1806
|
+
mode,
|
|
1807
|
+
previousMode: this.currentPermissionMode,
|
|
1808
|
+
});
|
|
1809
|
+
this.currentPermissionMode = mode;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1813
|
+
// Private: Timeline
|
|
1814
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1815
|
+
addTimelineFromSessionEvent(event) {
|
|
1816
|
+
// Hard noise: skip entirely
|
|
1817
|
+
if (isHardNoise(event))
|
|
1818
|
+
return;
|
|
1819
|
+
let tlType;
|
|
1820
|
+
let description;
|
|
1821
|
+
let noiseLevel = 'system';
|
|
1822
|
+
const metadata = {};
|
|
1823
|
+
switch (event.type) {
|
|
1824
|
+
case 'user':
|
|
1825
|
+
tlType = 'user_prompt';
|
|
1826
|
+
description = this.extractTextContent(event) ?? 'User prompt';
|
|
1827
|
+
noiseLevel = 'user';
|
|
1828
|
+
break;
|
|
1829
|
+
case 'assistant':
|
|
1830
|
+
tlType = 'assistant_response';
|
|
1831
|
+
description = this.extractTextContent(event) ?? 'Assistant response';
|
|
1832
|
+
noiseLevel = 'ai';
|
|
1833
|
+
if (event.message.model)
|
|
1834
|
+
metadata.model = event.message.model;
|
|
1835
|
+
{
|
|
1836
|
+
// Cache-inclusive, like every other token total (see tokenSummary).
|
|
1837
|
+
const usage = extractNormalizedUsage(event);
|
|
1838
|
+
if (usage)
|
|
1839
|
+
metadata.tokenCount = usage.totalTokens;
|
|
1840
|
+
}
|
|
1841
|
+
break;
|
|
1842
|
+
case 'tool_use': {
|
|
1843
|
+
tlType = 'tool_call';
|
|
1844
|
+
// Use rich tool summary formatter
|
|
1845
|
+
const toolName = event.tool?.name ?? 'unknown';
|
|
1846
|
+
const toolSummary = event.tool?.input ? formatToolSummary(toolName, event.tool.input) : '';
|
|
1847
|
+
description = toolSummary ? `${toolName}: ${toolSummary}` : toolName;
|
|
1848
|
+
noiseLevel = 'system';
|
|
1849
|
+
if (event.tool)
|
|
1850
|
+
metadata.toolName = event.tool.name;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
case 'tool_result':
|
|
1854
|
+
tlType = 'tool_result';
|
|
1855
|
+
description = event.result ? `Result for tool call` : 'Tool result';
|
|
1856
|
+
noiseLevel = 'noise';
|
|
1857
|
+
if (event.result?.is_error)
|
|
1858
|
+
metadata.isError = true;
|
|
1859
|
+
break;
|
|
1860
|
+
case 'summary':
|
|
1861
|
+
tlType = 'compaction';
|
|
1862
|
+
description = 'Context compacted';
|
|
1863
|
+
noiseLevel = 'system';
|
|
1864
|
+
break;
|
|
1865
|
+
case 'system':
|
|
1866
|
+
tlType = 'session_start';
|
|
1867
|
+
description =
|
|
1868
|
+
this.extractTextContent(event) ?? event.message?.sourceLabel ?? 'System event';
|
|
1869
|
+
noiseLevel = 'system';
|
|
1870
|
+
break;
|
|
1871
|
+
default:
|
|
1872
|
+
tlType = 'session_start';
|
|
1873
|
+
description = 'Event';
|
|
1874
|
+
break;
|
|
1875
|
+
}
|
|
1876
|
+
// Truncate description for timeline
|
|
1877
|
+
if (description.length > 200) {
|
|
1878
|
+
description = description.substring(0, 197) + '...';
|
|
1879
|
+
}
|
|
1880
|
+
// Classify noise
|
|
1881
|
+
const softNoiseReason = getSoftNoiseReason(event);
|
|
1882
|
+
const messageClassification = classifyMessage(event);
|
|
1883
|
+
this.timeline.push({
|
|
1884
|
+
type: tlType,
|
|
1885
|
+
timestamp: event.timestamp,
|
|
1886
|
+
description,
|
|
1887
|
+
noiseLevel,
|
|
1888
|
+
softNoiseReason: softNoiseReason ?? undefined,
|
|
1889
|
+
messageClassification,
|
|
1890
|
+
isSidechain: event.isSidechain,
|
|
1891
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1892
|
+
});
|
|
1893
|
+
// Cap timeline
|
|
1894
|
+
if (this.timeline.length > this.timelineCap) {
|
|
1895
|
+
this.timeline.shift();
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
addTimelineFromFollowEvent(event) {
|
|
1899
|
+
// Hard noise: skip entirely
|
|
1900
|
+
if (isHardNoiseFollowEvent(event))
|
|
1901
|
+
return;
|
|
1902
|
+
let tlType;
|
|
1903
|
+
let description = event.summary || '';
|
|
1904
|
+
let noiseLevel = 'system';
|
|
1905
|
+
const metadata = {};
|
|
1906
|
+
switch (event.type) {
|
|
1907
|
+
case 'user':
|
|
1908
|
+
tlType = 'user_prompt';
|
|
1909
|
+
noiseLevel = 'user';
|
|
1910
|
+
break;
|
|
1911
|
+
case 'assistant':
|
|
1912
|
+
tlType = 'assistant_response';
|
|
1913
|
+
noiseLevel = 'ai';
|
|
1914
|
+
if (event.model)
|
|
1915
|
+
metadata.model = event.model;
|
|
1916
|
+
if (event.tokens) {
|
|
1917
|
+
metadata.tokenCount =
|
|
1918
|
+
event.tokens.input +
|
|
1919
|
+
event.tokens.output +
|
|
1920
|
+
(event.cacheTokens?.read ?? 0) +
|
|
1921
|
+
(event.cacheTokens?.write ?? 0);
|
|
1922
|
+
}
|
|
1923
|
+
break;
|
|
1924
|
+
case 'tool_use':
|
|
1925
|
+
tlType = 'tool_call';
|
|
1926
|
+
noiseLevel = 'system';
|
|
1927
|
+
if (event.toolName)
|
|
1928
|
+
metadata.toolName = event.toolName;
|
|
1929
|
+
break;
|
|
1930
|
+
case 'tool_result':
|
|
1931
|
+
tlType = 'tool_result';
|
|
1932
|
+
noiseLevel = 'noise';
|
|
1933
|
+
break;
|
|
1934
|
+
case 'summary':
|
|
1935
|
+
tlType = 'compaction';
|
|
1936
|
+
noiseLevel = 'system';
|
|
1937
|
+
break;
|
|
1938
|
+
default:
|
|
1939
|
+
tlType = 'session_start';
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1942
|
+
// Truncate description for timeline
|
|
1943
|
+
if (description.length > 200) {
|
|
1944
|
+
description = description.substring(0, 197) + '...';
|
|
1945
|
+
}
|
|
1946
|
+
// Classify message
|
|
1947
|
+
const messageClassification = classifyFollowEvent(event);
|
|
1948
|
+
this.timeline.push({
|
|
1949
|
+
type: tlType,
|
|
1950
|
+
timestamp: event.timestamp,
|
|
1951
|
+
description,
|
|
1952
|
+
noiseLevel,
|
|
1953
|
+
messageClassification,
|
|
1954
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
1955
|
+
});
|
|
1956
|
+
// Cap timeline
|
|
1957
|
+
if (this.timeline.length > this.timelineCap) {
|
|
1958
|
+
this.timeline.shift();
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1962
|
+
// Private: Helper methods
|
|
1963
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1964
|
+
estimateTokens(text) {
|
|
1965
|
+
return estimateTextTokens(text).count;
|
|
1966
|
+
}
|
|
1967
|
+
hasTextContent(event) {
|
|
1968
|
+
const content = event.message?.content;
|
|
1969
|
+
if (typeof content === 'string')
|
|
1970
|
+
return content.length > 0;
|
|
1971
|
+
if (Array.isArray(content)) {
|
|
1972
|
+
return content.some((b) => b.type === 'text' && typeof b.text === 'string' && b.text.length > 0);
|
|
1973
|
+
}
|
|
1974
|
+
return false;
|
|
1975
|
+
}
|
|
1976
|
+
extractTextContent(event) {
|
|
1977
|
+
const content = event.message?.content;
|
|
1978
|
+
if (typeof content === 'string')
|
|
1979
|
+
return content;
|
|
1980
|
+
if (Array.isArray(content)) {
|
|
1981
|
+
for (const block of content) {
|
|
1982
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
1983
|
+
return block.text;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
extractTaskIdFromResult(content) {
|
|
1990
|
+
const str = typeof content === 'string' ? content : JSON.stringify(content || '');
|
|
1991
|
+
const taskMatch = str.match(/Task #(\d+)/i);
|
|
1992
|
+
if (taskMatch)
|
|
1993
|
+
return taskMatch[1];
|
|
1994
|
+
const jsonMatch = str.match(/"taskId"\s*:\s*"?(\d+)"?/i);
|
|
1995
|
+
if (jsonMatch)
|
|
1996
|
+
return jsonMatch[1];
|
|
1997
|
+
return null;
|
|
1998
|
+
}
|
|
1999
|
+
isSystemPromptContent(text) {
|
|
2000
|
+
return text.includes('<system-reminder>') || text.includes('CLAUDE.md');
|
|
2001
|
+
}
|
|
2002
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2003
|
+
// Private: Gonzo/Lazyjournal analytics
|
|
2004
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2005
|
+
extractToolNameFromEvent(event) {
|
|
2006
|
+
const content = event.message?.content;
|
|
2007
|
+
if (!Array.isArray(content))
|
|
2008
|
+
return undefined;
|
|
2009
|
+
for (const block of content) {
|
|
2010
|
+
if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
2011
|
+
return block.name;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
return undefined;
|
|
2015
|
+
}
|
|
2016
|
+
/** Feed event data into frequency trackers, pattern extractor, and heatmap. */
|
|
2017
|
+
feedAnalytics(timestamp, summary, toolName) {
|
|
2018
|
+
// Heatmap: record every event
|
|
2019
|
+
this.heatmapTracker.record(timestamp);
|
|
2020
|
+
// Tool frequency
|
|
2021
|
+
if (toolName) {
|
|
2022
|
+
this.toolFrequency.increment(toolName, timestamp);
|
|
2023
|
+
}
|
|
2024
|
+
// Word frequency: extract significant words from summary
|
|
2025
|
+
if (summary) {
|
|
2026
|
+
const words = summary.split(/\s+/).filter((w) => w.length > 2);
|
|
2027
|
+
for (const word of words) {
|
|
2028
|
+
this.wordFrequency.increment(word.toLowerCase(), timestamp);
|
|
2029
|
+
}
|
|
2030
|
+
// Pattern extraction
|
|
2031
|
+
this.patternExtractor.add(summary);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|