@link-assistant/hive-mind 1.38.1 → 1.38.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +113 -123
- package/src/claude.lib.mjs +51 -14
- package/src/github.lib.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.38.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 290139f: fix: correct cost and token/context budget calculations (#1501)
|
|
8
|
+
- Deduplicate JSONL session entries by message ID to fix inflated token counts caused by upstream anthropics/claude-code#6805
|
|
9
|
+
- Show peak context window usage (max single-request fill) instead of cumulative sum which produced nonsensical percentages like 7516%
|
|
10
|
+
- Add "Total tokens processed" as a separate cumulative metric for session throughput visibility
|
|
11
|
+
- Add verbose logging for JSONL deduplication stats and peak context values
|
|
12
|
+
|
|
3
13
|
## 1.38.1
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -14,6 +14,8 @@ export const createEmptySubSessionUsage = () => ({
|
|
|
14
14
|
cacheReadTokens: 0,
|
|
15
15
|
outputTokens: 0,
|
|
16
16
|
messageCount: 0,
|
|
17
|
+
peakContextUsage: 0,
|
|
18
|
+
peakOutputUsage: 0,
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -136,173 +138,161 @@ export const displayCostComparison = async (publicCost, anthropicCost, log) => {
|
|
|
136
138
|
/**
|
|
137
139
|
* Display token budget statistics (context window usage and ratios)
|
|
138
140
|
* @param {Object} usage - Usage data for a model
|
|
141
|
+
* @param {Object} tokenUsage - Full token usage data (with subSessions)
|
|
139
142
|
* @param {Function} log - Logging function
|
|
140
143
|
*/
|
|
141
|
-
export const displayBudgetStats = async (usage, log) => {
|
|
144
|
+
export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
142
145
|
const modelInfo = usage.modelInfo;
|
|
143
146
|
if (!modelInfo?.limit) {
|
|
144
147
|
await log('\n ⚠️ Budget stats not available (no model limits found)');
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
await log('\n 📊
|
|
151
|
+
await log('\n 📊 Context and tokens usage:');
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const totalInputUsed = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
|
|
155
|
-
const contextUsageRatio = totalInputUsed / contextLimit;
|
|
156
|
-
const contextUsagePercent = (contextUsageRatio * 100).toFixed(2);
|
|
153
|
+
const contextLimit = modelInfo.limit.context;
|
|
154
|
+
const outputLimit = modelInfo.limit.output;
|
|
155
|
+
const subSessions = tokenUsage?.subSessions || [];
|
|
156
|
+
const hasMultipleSubSessions = subSessions.length > 1;
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
await log(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
* Display sub-session breakdown when compactification events occurred (Issue #1491)
|
|
183
|
-
* @param {Object} tokenUsage - Token usage data with subSessions and compactifications
|
|
184
|
-
* @param {Object} modelInfo - Model info with context/output limits
|
|
185
|
-
* @param {Function} log - Logging function
|
|
186
|
-
*/
|
|
187
|
-
export const displaySubSessionStats = async (tokenUsage, modelInfo, log) => {
|
|
188
|
-
if (!tokenUsage.subSessions || !tokenUsage.compactifications) return;
|
|
189
|
-
|
|
190
|
-
const contextLimit = modelInfo?.limit?.context;
|
|
191
|
-
await log(`\n 🔄 Compactification events: ${tokenUsage.compactifications.length}`);
|
|
192
|
-
|
|
193
|
-
for (let i = 0; i < tokenUsage.subSessions.length; i++) {
|
|
194
|
-
const sub = tokenUsage.subSessions[i];
|
|
195
|
-
const totalInput = sub.inputTokens + sub.cacheCreationTokens + sub.cacheReadTokens;
|
|
196
|
-
const label = i === 0 ? 'Initial session' : `After compactification #${i}`;
|
|
197
|
-
|
|
198
|
-
await log(` Sub-session ${i + 1} (${label}):`);
|
|
199
|
-
await log(` Messages: ${sub.messageCount}`);
|
|
200
|
-
await log(` Context used: ${formatNumber(totalInput)} tokens`);
|
|
158
|
+
if (hasMultipleSubSessions) {
|
|
159
|
+
await log(' Sub sessions (between compact events):');
|
|
160
|
+
for (let i = 0; i < subSessions.length; i++) {
|
|
161
|
+
const sub = subSessions[i];
|
|
162
|
+
const subPeak = sub.peakContextUsage || 0;
|
|
163
|
+
let line = ` ${i + 1}. `;
|
|
164
|
+
if (contextLimit && subPeak > 0) {
|
|
165
|
+
const pct = ((subPeak / contextLimit) * 100).toFixed(0);
|
|
166
|
+
line += `${formatNumber(subPeak)} / ${formatNumber(contextLimit)} input tokens (${pct}%)`;
|
|
167
|
+
} else {
|
|
168
|
+
const subTotal = sub.inputTokens + sub.cacheCreationTokens + sub.cacheReadTokens;
|
|
169
|
+
line += `${formatNumber(subTotal)} input tokens`;
|
|
170
|
+
}
|
|
171
|
+
if (outputLimit) {
|
|
172
|
+
const outPct = ((sub.outputTokens / outputLimit) * 100).toFixed(0);
|
|
173
|
+
line += `; ${formatNumber(sub.outputTokens)} / ${formatNumber(outputLimit)} output tokens (${outPct}%)`;
|
|
174
|
+
} else {
|
|
175
|
+
line += `; ${formatNumber(sub.outputTokens)} output tokens`;
|
|
176
|
+
}
|
|
177
|
+
await log(line);
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
// Single sub-session: simplified format
|
|
181
|
+
const peakContext = usage.peakContextUsage || 0;
|
|
201
182
|
if (contextLimit) {
|
|
202
|
-
|
|
203
|
-
|
|
183
|
+
if (peakContext > 0) {
|
|
184
|
+
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
185
|
+
await log(` Max context window: ${formatNumber(peakContext)} / ${formatNumber(contextLimit)} input tokens (${pct}%)`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (outputLimit) {
|
|
189
|
+
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
190
|
+
await log(` Max output tokens: ${formatNumber(usage.outputTokens)} / ${formatNumber(outputLimit)} output tokens (${outPct}%)`);
|
|
204
191
|
}
|
|
205
|
-
await log(` Output: ${formatNumber(sub.outputTokens)} tokens`);
|
|
206
192
|
}
|
|
207
193
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
194
|
+
// Cumulative totals
|
|
195
|
+
const totalInputNonCached = usage.inputTokens + usage.cacheCreationTokens;
|
|
196
|
+
const cachedTokens = usage.cacheReadTokens;
|
|
197
|
+
let totalLine = ` Total input tokens: ${formatNumber(totalInputNonCached)}`;
|
|
198
|
+
if (cachedTokens > 0) totalLine += ` + ${formatNumber(cachedTokens)} cached`;
|
|
199
|
+
await log(totalLine);
|
|
200
|
+
await log(` Total output tokens: ${formatNumber(usage.outputTokens)}`);
|
|
215
201
|
};
|
|
216
202
|
|
|
217
203
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* @
|
|
221
|
-
* @param {Object} jsonlTokenUsage - Token usage calculated from JSONL session file
|
|
222
|
-
* @param {Function} log - Logging function
|
|
204
|
+
* Format a token count with K/M suffix for compact display
|
|
205
|
+
* @param {number} tokens - Token count
|
|
206
|
+
* @returns {string} Formatted string like "850K" or "1.5M"
|
|
223
207
|
*/
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const jsonlTotal = jsonlTokenUsage.inputTokens + jsonlTokenUsage.cacheCreationTokens + jsonlTokenUsage.outputTokens;
|
|
229
|
-
|
|
230
|
-
await log('\n 🔍 Token calculation comparison:');
|
|
231
|
-
await log(` Stream JSON events: ${formatNumber(streamTotal)} tokens (${streamTokenUsage.eventCount} events)`);
|
|
232
|
-
await log(` JSONL session file: ${formatNumber(jsonlTotal)} tokens`);
|
|
233
|
-
|
|
234
|
-
if (streamTotal !== jsonlTotal) {
|
|
235
|
-
const diff = jsonlTotal - streamTotal;
|
|
236
|
-
const pct = streamTotal > 0 ? ((diff / streamTotal) * 100).toFixed(2) : 'N/A';
|
|
237
|
-
await log(` Difference: ${formatNumber(Math.abs(diff))} tokens (${diff > 0 ? '+' : ''}${pct}%)`);
|
|
238
|
-
} else {
|
|
239
|
-
await log(' Match: calculations are consistent');
|
|
240
|
-
}
|
|
208
|
+
const formatTokensCompact = tokens => {
|
|
209
|
+
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(tokens % 1000000 === 0 ? 0 : 1)}M`;
|
|
210
|
+
if (tokens >= 1000) return `${(tokens / 1000).toFixed(tokens % 1000 === 0 ? 0 : 1)}K`;
|
|
211
|
+
return tokens.toLocaleString();
|
|
241
212
|
};
|
|
242
213
|
|
|
243
214
|
/**
|
|
244
|
-
* Build budget stats string for GitHub PR comments (Issue #1491)
|
|
245
|
-
*
|
|
215
|
+
* Build budget stats string for GitHub PR comments (Issue #1491, #1501)
|
|
216
|
+
* Format requested by user: sub-sessions between compactification events,
|
|
217
|
+
* per-model breakdown, cumulative totals with cached tokens shown separately.
|
|
246
218
|
* @param {Object} tokenUsage - Token usage data from calculateSessionTokens
|
|
247
|
-
* @param {Object|null} streamTokenUsage - Token usage from stream JSON events
|
|
219
|
+
* @param {Object|null} streamTokenUsage - Token usage from stream JSON events (used for comparison, not displayed)
|
|
248
220
|
* @returns {string} Formatted markdown string for PR comment
|
|
249
221
|
*/
|
|
250
|
-
export const buildBudgetStatsString =
|
|
222
|
+
export const buildBudgetStatsString = tokenUsage => {
|
|
251
223
|
if (!tokenUsage) return '';
|
|
252
224
|
|
|
253
|
-
let stats = '\n\n### 📊 **
|
|
225
|
+
let stats = '\n\n### 📊 **Context and tokens usage:**';
|
|
254
226
|
|
|
255
227
|
// Per-model breakdown
|
|
256
228
|
if (tokenUsage.modelUsage) {
|
|
257
229
|
const modelIds = Object.keys(tokenUsage.modelUsage);
|
|
230
|
+
const isMultiModel = modelIds.length > 1;
|
|
231
|
+
|
|
258
232
|
for (const modelId of modelIds) {
|
|
259
233
|
const usage = tokenUsage.modelUsage[modelId];
|
|
260
234
|
const modelName = usage.modelName || modelId;
|
|
261
235
|
const contextLimit = usage.modelInfo?.limit?.context;
|
|
262
236
|
const outputLimit = usage.modelInfo?.limit?.output;
|
|
263
|
-
const totalInput = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
|
|
264
237
|
|
|
265
|
-
if (
|
|
238
|
+
if (isMultiModel) stats += `\n\n**${modelName}:**`;
|
|
266
239
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
} else {
|
|
271
|
-
stats += `\n- Context tokens used: ${totalInput.toLocaleString()}`;
|
|
272
|
-
}
|
|
240
|
+
// Sub-session display (Issue #1501: show per sub-session stats)
|
|
241
|
+
const subSessions = tokenUsage.subSessions || [];
|
|
242
|
+
const hasMultipleSubSessions = subSessions.length > 1;
|
|
273
243
|
|
|
274
|
-
if (
|
|
275
|
-
|
|
276
|
-
stats +=
|
|
244
|
+
if (hasMultipleSubSessions) {
|
|
245
|
+
// Multiple sub-sessions: show numbered list
|
|
246
|
+
stats += '\n\nSub sessions (between compact events):';
|
|
247
|
+
for (let i = 0; i < subSessions.length; i++) {
|
|
248
|
+
const sub = subSessions[i];
|
|
249
|
+
const subPeakContext = sub.peakContextUsage || 0;
|
|
250
|
+
const subTotalInput = sub.inputTokens + sub.cacheCreationTokens + sub.cacheReadTokens;
|
|
251
|
+
let line = `\n${i + 1}. `;
|
|
252
|
+
if (contextLimit && subPeakContext > 0) {
|
|
253
|
+
const pct = ((subPeakContext / contextLimit) * 100).toFixed(0);
|
|
254
|
+
line += `${formatTokensCompact(subPeakContext)} / ${formatTokensCompact(contextLimit)} input tokens (${pct}%)`;
|
|
255
|
+
} else {
|
|
256
|
+
line += `${formatTokensCompact(subTotalInput)} input tokens`;
|
|
257
|
+
}
|
|
258
|
+
if (outputLimit) {
|
|
259
|
+
const outPct = ((sub.outputTokens / outputLimit) * 100).toFixed(0);
|
|
260
|
+
line += `; ${formatTokensCompact(sub.outputTokens)} / ${formatTokensCompact(outputLimit)} output tokens (${outPct}%)`;
|
|
261
|
+
} else {
|
|
262
|
+
line += `; ${formatTokensCompact(sub.outputTokens)} output tokens`;
|
|
263
|
+
}
|
|
264
|
+
stats += line;
|
|
265
|
+
}
|
|
277
266
|
} else {
|
|
278
|
-
|
|
267
|
+
// Single sub-session (or no sub-sessions): simplified format
|
|
268
|
+
const peakContext = usage.peakContextUsage || 0;
|
|
269
|
+
if (contextLimit) {
|
|
270
|
+
if (peakContext > 0) {
|
|
271
|
+
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
272
|
+
stats += `\n- Max context window: ${formatTokensCompact(peakContext)} / ${formatTokensCompact(contextLimit)} input tokens (${pct}%)`;
|
|
273
|
+
} else {
|
|
274
|
+
const totalInput = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
|
|
275
|
+
const pct = ((totalInput / contextLimit) * 100).toFixed(0);
|
|
276
|
+
stats += `\n- Context window: ${formatTokensCompact(totalInput)} / ${formatTokensCompact(contextLimit)} tokens (${pct}%)`;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (outputLimit) {
|
|
280
|
+
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
281
|
+
stats += `\n- Max output tokens: ${formatTokensCompact(usage.outputTokens)} / ${formatTokensCompact(outputLimit)} output tokens (${outPct}%)`;
|
|
282
|
+
}
|
|
279
283
|
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
284
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const label = i === 0 ? 'initial' : `after compactification #${i}`;
|
|
290
|
-
stats += `\n - Sub-session ${i + 1} (${label}): ${totalInput.toLocaleString()} context, ${sub.outputTokens.toLocaleString()} output, ${sub.messageCount} messages`;
|
|
285
|
+
// Cumulative totals: input tokens + cached shown separately
|
|
286
|
+
const totalInputNonCached = usage.inputTokens + usage.cacheCreationTokens;
|
|
287
|
+
const cachedTokens = usage.cacheReadTokens;
|
|
288
|
+
stats += `\n\nTotal input tokens: ${formatTokensCompact(totalInputNonCached)}`;
|
|
289
|
+
if (cachedTokens > 0) stats += ` + ${formatTokensCompact(cachedTokens)} cached`;
|
|
290
|
+
stats += `\nTotal output tokens: ${formatTokensCompact(usage.outputTokens)} output`;
|
|
291
291
|
}
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
// Stream vs JSONL comparison
|
|
295
|
-
|
|
296
|
-
const streamTotal = streamTokenUsage.inputTokens + streamTokenUsage.cacheCreationTokens + streamTokenUsage.outputTokens;
|
|
297
|
-
const jsonlTotal = tokenUsage.inputTokens + tokenUsage.cacheCreationTokens + tokenUsage.outputTokens;
|
|
298
|
-
stats += `\n- Own calculation (stream): ${streamTotal.toLocaleString()} tokens (${streamTokenUsage.eventCount} events)`;
|
|
299
|
-
stats += `\n- JSONL calculation: ${jsonlTotal.toLocaleString()} tokens`;
|
|
300
|
-
if (streamTotal !== jsonlTotal) {
|
|
301
|
-
const diff = jsonlTotal - streamTotal;
|
|
302
|
-
const pct = streamTotal > 0 ? ((diff / streamTotal) * 100).toFixed(2) : 'N/A';
|
|
303
|
-
stats += ` (diff: ${diff > 0 ? '+' : ''}${pct}%)`;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
294
|
+
// Stream vs JSONL comparison — kept for internal diagnostics only in verbose/debug mode
|
|
295
|
+
// Not shown to users per feedback (Issue #1501 PR comment)
|
|
306
296
|
|
|
307
297
|
return stats;
|
|
308
298
|
};
|
package/src/claude.lib.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { timeouts, retryLimits, claudeCode, getClaudeEnv, getThinkingLevelToToke
|
|
|
12
12
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
13
13
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
14
14
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
15
|
-
import { displayBudgetStats,
|
|
15
|
+
import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison } from './claude.budget-stats.lib.mjs';
|
|
16
16
|
import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
17
17
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
18
18
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
@@ -497,6 +497,15 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
497
497
|
}
|
|
498
498
|
// Initialize per-model usage tracking
|
|
499
499
|
const modelUsage = {};
|
|
500
|
+
// Issue #1501: Deduplicate JSONL entries by message ID (upstream: anthropics/claude-code#6805)
|
|
501
|
+
// Claude Code's stream-json mode splits single API responses with multiple content blocks
|
|
502
|
+
// into separate JSONL entries, each with the same message ID and identical usage stats.
|
|
503
|
+
const seenMessageIds = new Set();
|
|
504
|
+
let duplicateCount = 0;
|
|
505
|
+
// Issue #1501: Track peak context usage per request (not cumulative)
|
|
506
|
+
// The context window limit is per-request, so we track the max single-request fill.
|
|
507
|
+
const peakContextByModel = {};
|
|
508
|
+
let globalPeakContext = 0;
|
|
500
509
|
// Issue #1491: Track sub-sessions between compactification events
|
|
501
510
|
const subSessions = [];
|
|
502
511
|
let currentSubSession = createEmptySubSessionUsage();
|
|
@@ -524,14 +533,39 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
524
533
|
continue;
|
|
525
534
|
}
|
|
526
535
|
if (entry.message && entry.message.usage && entry.message.model) {
|
|
536
|
+
// Issue #1501: Skip duplicate JSONL entries (same message ID = same API response)
|
|
537
|
+
const msgId = entry.message.id;
|
|
538
|
+
if (msgId) {
|
|
539
|
+
if (seenMessageIds.has(msgId)) {
|
|
540
|
+
duplicateCount++;
|
|
541
|
+
continue; // Skip — already counted this message's usage
|
|
542
|
+
}
|
|
543
|
+
seenMessageIds.add(msgId);
|
|
544
|
+
}
|
|
527
545
|
accumulateModelUsage(modelUsage, entry);
|
|
528
|
-
// Issue #
|
|
546
|
+
// Issue #1501: Track peak context usage per single API request
|
|
529
547
|
const usage = entry.message.usage;
|
|
548
|
+
const requestContext = (usage.input_tokens || 0) + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
549
|
+
const model = entry.message.model;
|
|
550
|
+
if (requestContext > (peakContextByModel[model] || 0)) {
|
|
551
|
+
peakContextByModel[model] = requestContext;
|
|
552
|
+
}
|
|
553
|
+
if (requestContext > globalPeakContext) {
|
|
554
|
+
globalPeakContext = requestContext;
|
|
555
|
+
}
|
|
556
|
+
// Issue #1491: Also track per-sub-session usage
|
|
530
557
|
if (usage.input_tokens) currentSubSession.inputTokens += usage.input_tokens;
|
|
531
558
|
if (usage.cache_creation_input_tokens) currentSubSession.cacheCreationTokens += usage.cache_creation_input_tokens;
|
|
532
559
|
if (usage.cache_read_input_tokens) currentSubSession.cacheReadTokens += usage.cache_read_input_tokens;
|
|
533
560
|
if (usage.output_tokens) currentSubSession.outputTokens += usage.output_tokens;
|
|
534
561
|
currentSubSession.messageCount++;
|
|
562
|
+
// Issue #1501: Track peak context and output per sub-session
|
|
563
|
+
if (requestContext > currentSubSession.peakContextUsage) {
|
|
564
|
+
currentSubSession.peakContextUsage = requestContext;
|
|
565
|
+
}
|
|
566
|
+
if ((usage.output_tokens || 0) > currentSubSession.peakOutputUsage) {
|
|
567
|
+
currentSubSession.peakOutputUsage = usage.output_tokens || 0;
|
|
568
|
+
}
|
|
535
569
|
}
|
|
536
570
|
} catch {
|
|
537
571
|
// Skip lines that aren't valid JSON
|
|
@@ -561,6 +595,8 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
561
595
|
// Calculate cost for each model and store all characteristics
|
|
562
596
|
for (const [modelId, usage] of Object.entries(modelUsage)) {
|
|
563
597
|
const modelInfo = modelInfoMap[modelId];
|
|
598
|
+
// Issue #1501: Attach peak context usage per model
|
|
599
|
+
usage.peakContextUsage = peakContextByModel[modelId] || 0;
|
|
564
600
|
// Calculate cost using pricing API
|
|
565
601
|
if (modelInfo) {
|
|
566
602
|
const costData = calculateModelCost(usage, modelInfo, true);
|
|
@@ -604,8 +640,11 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
604
640
|
outputTokens: totalOutputTokens,
|
|
605
641
|
totalTokens,
|
|
606
642
|
totalCostUSD: hasCostData ? totalCostUSD : null,
|
|
607
|
-
// Issue #
|
|
608
|
-
|
|
643
|
+
// Issue #1501: Peak context usage (max single-request fill) and dedup stats
|
|
644
|
+
peakContextUsage: globalPeakContext,
|
|
645
|
+
duplicateEntriesSkipped: duplicateCount,
|
|
646
|
+
// Issue #1491/#1501: Sub-session and compactification data (always include for display)
|
|
647
|
+
subSessions,
|
|
609
648
|
compactifications: compactifications.length > 0 ? compactifications : null,
|
|
610
649
|
};
|
|
611
650
|
} catch (readError) {
|
|
@@ -1248,6 +1287,13 @@ export const executeClaudeCommand = async params => {
|
|
|
1248
1287
|
try {
|
|
1249
1288
|
const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
|
|
1250
1289
|
if (tokenUsage) {
|
|
1290
|
+
// Issue #1501: Log deduplication stats in verbose mode
|
|
1291
|
+
if (tokenUsage.duplicateEntriesSkipped > 0) {
|
|
1292
|
+
await log(`\n⚠️ JSONL deduplication: skipped ${tokenUsage.duplicateEntriesSkipped} duplicate entries (upstream: anthropics/claude-code#6805)`, { verbose: true });
|
|
1293
|
+
}
|
|
1294
|
+
if (tokenUsage.peakContextUsage > 0) {
|
|
1295
|
+
await log(`📊 Peak single-request context: ${formatNumber(tokenUsage.peakContextUsage)} tokens`, { verbose: true });
|
|
1296
|
+
}
|
|
1251
1297
|
await log('\n💰 Token Usage Summary:');
|
|
1252
1298
|
// Display per-model breakdown
|
|
1253
1299
|
if (tokenUsage.modelUsage) {
|
|
@@ -1258,18 +1304,9 @@ export const executeClaudeCommand = async params => {
|
|
|
1258
1304
|
await displayModelUsage(usage, log);
|
|
1259
1305
|
// Display budget stats if flag is enabled
|
|
1260
1306
|
if (argv.tokensBudgetStats && usage.modelInfo?.limit) {
|
|
1261
|
-
await displayBudgetStats(usage, log);
|
|
1307
|
+
await displayBudgetStats(usage, tokenUsage, log);
|
|
1262
1308
|
}
|
|
1263
1309
|
}
|
|
1264
|
-
// Issue #1491: Display sub-session breakdown if compactification occurred
|
|
1265
|
-
if (argv.tokensBudgetStats && tokenUsage.subSessions) {
|
|
1266
|
-
const primaryModelInfo = Object.values(tokenUsage.modelUsage).find(u => u.modelInfo?.limit)?.modelInfo;
|
|
1267
|
-
await displaySubSessionStats(tokenUsage, primaryModelInfo, log);
|
|
1268
|
-
}
|
|
1269
|
-
// Issue #1491: Display stream vs JSONL token comparison
|
|
1270
|
-
if (argv.tokensBudgetStats && streamTokenUsage.eventCount > 0) {
|
|
1271
|
-
await displayTokenComparison(streamTokenUsage, tokenUsage, log);
|
|
1272
|
-
}
|
|
1273
1310
|
// Show totals if multiple models were used
|
|
1274
1311
|
if (modelIds.length > 1) {
|
|
1275
1312
|
await log('\n 📈 Total across all models:');
|
package/src/github.lib.mjs
CHANGED
|
@@ -368,7 +368,7 @@ export async function attachLogToGitHub(options) {
|
|
|
368
368
|
resultModelUsage = null, // Issue #1454
|
|
369
369
|
budgetStatsData = null, // Issue #1491: budget stats for comment
|
|
370
370
|
} = options;
|
|
371
|
-
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage
|
|
371
|
+
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage) : '';
|
|
372
372
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
373
373
|
const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
|
|
374
374
|
try {
|