@link-assistant/hive-mind 1.37.4 → 1.38.1
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 +12 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +258 -0
- package/src/claude.lib.mjs +60 -125
- package/src/github.lib.mjs +9 -9
- package/src/solve.mjs +7 -9
- package/src/solve.results.lib.mjs +19 -1
- package/src/telegram-bot.mjs +37 -39
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.38.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1525ecb: fix: prevent 'Failed to send formatted message' Telegram error by adding safeReply helper and escaping unescaped Markdown in bot messages
|
|
8
|
+
|
|
9
|
+
## 1.38.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- ee331ef: Enhance --tokens-budget-stats with sub-session tracking, stream comparison, and GitHub comment display
|
|
14
|
+
|
|
3
15
|
## 1.37.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -4,6 +4,135 @@
|
|
|
4
4
|
|
|
5
5
|
import { formatNumber } from './claude.lib.mjs';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Helper: creates a fresh sub-session usage object for tracking tokens between compactification events
|
|
9
|
+
* @returns {Object} Empty sub-session usage structure
|
|
10
|
+
*/
|
|
11
|
+
export const createEmptySubSessionUsage = () => ({
|
|
12
|
+
inputTokens: 0,
|
|
13
|
+
cacheCreationTokens: 0,
|
|
14
|
+
cacheReadTokens: 0,
|
|
15
|
+
outputTokens: 0,
|
|
16
|
+
messageCount: 0,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper: accumulates token usage from a JSONL entry into a model usage map
|
|
21
|
+
* @param {Object} modelUsageMap - Map of model ID to usage data
|
|
22
|
+
* @param {Object} entry - Parsed JSONL entry with message.usage and message.model
|
|
23
|
+
*/
|
|
24
|
+
export const accumulateModelUsage = (modelUsageMap, entry) => {
|
|
25
|
+
const model = entry.message.model;
|
|
26
|
+
if (model.startsWith('<') && model.endsWith('>')) return; // Issue #1486: skip <synthetic> etc.
|
|
27
|
+
const usage = entry.message.usage;
|
|
28
|
+
if (!modelUsageMap[model]) {
|
|
29
|
+
modelUsageMap[model] = {
|
|
30
|
+
inputTokens: 0,
|
|
31
|
+
cacheCreationTokens: 0,
|
|
32
|
+
cacheCreation5mTokens: 0,
|
|
33
|
+
cacheCreation1hTokens: 0,
|
|
34
|
+
cacheReadTokens: 0,
|
|
35
|
+
outputTokens: 0,
|
|
36
|
+
webSearchRequests: 0,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (usage.input_tokens) modelUsageMap[model].inputTokens += usage.input_tokens;
|
|
40
|
+
if (usage.cache_creation_input_tokens) modelUsageMap[model].cacheCreationTokens += usage.cache_creation_input_tokens;
|
|
41
|
+
if (usage.cache_creation) {
|
|
42
|
+
if (usage.cache_creation.ephemeral_5m_input_tokens) modelUsageMap[model].cacheCreation5mTokens += usage.cache_creation.ephemeral_5m_input_tokens;
|
|
43
|
+
if (usage.cache_creation.ephemeral_1h_input_tokens) modelUsageMap[model].cacheCreation1hTokens += usage.cache_creation.ephemeral_1h_input_tokens;
|
|
44
|
+
}
|
|
45
|
+
if (usage.cache_read_input_tokens) modelUsageMap[model].cacheReadTokens += usage.cache_read_input_tokens;
|
|
46
|
+
if (usage.output_tokens) modelUsageMap[model].outputTokens += usage.output_tokens;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Display detailed model usage information
|
|
51
|
+
* @param {Object} usage - Usage data for a model
|
|
52
|
+
* @param {Function} log - Logging function
|
|
53
|
+
*/
|
|
54
|
+
export const displayModelUsage = async (usage, log) => {
|
|
55
|
+
// Show all model characteristics if available
|
|
56
|
+
if (usage.modelInfo) {
|
|
57
|
+
const info = usage.modelInfo;
|
|
58
|
+
const fields = [
|
|
59
|
+
{ label: 'Model ID', value: info.id },
|
|
60
|
+
{ label: 'Provider', value: info.provider || 'Unknown' },
|
|
61
|
+
{ label: 'Context window', value: info.limit?.context ? `${formatNumber(info.limit.context)} tokens` : null },
|
|
62
|
+
{ label: 'Max output', value: info.limit?.output ? `${formatNumber(info.limit.output)} tokens` : null },
|
|
63
|
+
{ label: 'Input modalities', value: info.modalities?.input?.join(', ') || 'N/A' },
|
|
64
|
+
{ label: 'Output modalities', value: info.modalities?.output?.join(', ') || 'N/A' },
|
|
65
|
+
{ label: 'Knowledge cutoff', value: info.knowledge },
|
|
66
|
+
{ label: 'Released', value: info.release_date },
|
|
67
|
+
{
|
|
68
|
+
label: 'Capabilities',
|
|
69
|
+
value: [info.attachment && 'Attachments', info.reasoning && 'Reasoning', info.temperature && 'Temperature', info.tool_call && 'Tool calls'].filter(Boolean).join(', ') || 'N/A',
|
|
70
|
+
},
|
|
71
|
+
{ label: 'Open weights', value: info.open_weights ? 'Yes' : 'No' },
|
|
72
|
+
];
|
|
73
|
+
for (const { label, value } of fields) {
|
|
74
|
+
if (value) await log(` ${label}: ${value}`);
|
|
75
|
+
}
|
|
76
|
+
await log('');
|
|
77
|
+
} else {
|
|
78
|
+
await log(' ⚠️ Model info not available\n');
|
|
79
|
+
}
|
|
80
|
+
// Show usage data
|
|
81
|
+
await log(' Usage:');
|
|
82
|
+
await log(` Input tokens: ${formatNumber(usage.inputTokens)}`);
|
|
83
|
+
if (usage.cacheCreationTokens > 0) {
|
|
84
|
+
await log(` Cache creation tokens: ${formatNumber(usage.cacheCreationTokens)}`);
|
|
85
|
+
}
|
|
86
|
+
if (usage.cacheReadTokens > 0) {
|
|
87
|
+
await log(` Cache read tokens: ${formatNumber(usage.cacheReadTokens)}`);
|
|
88
|
+
}
|
|
89
|
+
await log(` Output tokens: ${formatNumber(usage.outputTokens)}`);
|
|
90
|
+
if (usage.webSearchRequests > 0) {
|
|
91
|
+
await log(` Web search requests: ${usage.webSearchRequests}`);
|
|
92
|
+
}
|
|
93
|
+
// Show detailed cost calculation
|
|
94
|
+
if (usage.costUSD !== null && usage.costUSD !== undefined && usage.costBreakdown) {
|
|
95
|
+
await log('');
|
|
96
|
+
await log(' Cost Calculation (USD):');
|
|
97
|
+
const breakdown = usage.costBreakdown;
|
|
98
|
+
const types = [
|
|
99
|
+
{ key: 'input', label: 'Input' },
|
|
100
|
+
{ key: 'cacheWrite', label: 'Cache write' },
|
|
101
|
+
{ key: 'cacheRead', label: 'Cache read' },
|
|
102
|
+
{ key: 'output', label: 'Output' },
|
|
103
|
+
];
|
|
104
|
+
for (const { key, label } of types) {
|
|
105
|
+
if (breakdown[key].tokens > 0) {
|
|
106
|
+
await log(` ${label}: ${formatNumber(breakdown[key].tokens)} tokens × $${breakdown[key].costPerMillion}/M = $${breakdown[key].cost.toFixed(6)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
await log(' ─────────────────────────────────');
|
|
110
|
+
await log(` Total: $${usage.costUSD.toFixed(6)}`);
|
|
111
|
+
} else if (usage.modelInfo === null) {
|
|
112
|
+
await log('');
|
|
113
|
+
await log(' Cost: Not available (could not fetch pricing)');
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Display cost comparison between public pricing and Anthropic's official cost
|
|
119
|
+
* @param {number|null} publicCost - Public pricing estimate
|
|
120
|
+
* @param {number|null} anthropicCost - Anthropic's official cost
|
|
121
|
+
* @param {Function} log - Logging function
|
|
122
|
+
*/
|
|
123
|
+
export const displayCostComparison = async (publicCost, anthropicCost, log) => {
|
|
124
|
+
await log('\n 💰 Cost estimation:');
|
|
125
|
+
await log(` Public pricing estimate: ${publicCost !== null && publicCost !== undefined ? `$${publicCost.toFixed(6)} USD` : 'unknown'}`);
|
|
126
|
+
await log(` Calculated by Anthropic: ${anthropicCost !== null && anthropicCost !== undefined ? `$${anthropicCost.toFixed(6)} USD` : 'unknown'}`);
|
|
127
|
+
if (publicCost !== null && publicCost !== undefined && anthropicCost !== null && anthropicCost !== undefined) {
|
|
128
|
+
const difference = anthropicCost - publicCost;
|
|
129
|
+
const percentDiff = publicCost > 0 ? (difference / publicCost) * 100 : 0;
|
|
130
|
+
await log(` Difference: $${difference.toFixed(6)} (${percentDiff > 0 ? '+' : ''}${percentDiff.toFixed(2)}%)`);
|
|
131
|
+
} else {
|
|
132
|
+
await log(' Difference: unknown');
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
7
136
|
/**
|
|
8
137
|
* Display token budget statistics (context window usage and ratios)
|
|
9
138
|
* @param {Object} usage - Usage data for a model
|
|
@@ -48,3 +177,132 @@ export const displayBudgetStats = async (usage, log) => {
|
|
|
48
177
|
const totalSessionTokens = usage.inputTokens + usage.cacheCreationTokens + usage.outputTokens;
|
|
49
178
|
await log(` Total session tokens: ${formatNumber(totalSessionTokens)}`);
|
|
50
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`);
|
|
201
|
+
if (contextLimit) {
|
|
202
|
+
const pct = ((totalInput / contextLimit) * 100).toFixed(2);
|
|
203
|
+
await log(` Context usage: ${pct}% of ${formatNumber(contextLimit)}`);
|
|
204
|
+
}
|
|
205
|
+
await log(` Output: ${formatNumber(sub.outputTokens)} tokens`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Show compactification details
|
|
209
|
+
for (let i = 0; i < tokenUsage.compactifications.length; i++) {
|
|
210
|
+
const comp = tokenUsage.compactifications[i];
|
|
211
|
+
let detail = ` Compactification #${i + 1}: trigger=${comp.trigger}`;
|
|
212
|
+
if (comp.preTokens) detail += `, pre-compaction tokens=${formatNumber(comp.preTokens)}`;
|
|
213
|
+
await log(detail);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Display stream vs JSONL token comparison (Issue #1491)
|
|
219
|
+
* Shows independent calculation from stream events vs JSONL session file
|
|
220
|
+
* @param {Object} streamTokenUsage - Token usage accumulated from stream JSON events
|
|
221
|
+
* @param {Object} jsonlTokenUsage - Token usage calculated from JSONL session file
|
|
222
|
+
* @param {Function} log - Logging function
|
|
223
|
+
*/
|
|
224
|
+
export const displayTokenComparison = async (streamTokenUsage, jsonlTokenUsage, log) => {
|
|
225
|
+
if (!streamTokenUsage || !jsonlTokenUsage) return;
|
|
226
|
+
|
|
227
|
+
const streamTotal = streamTokenUsage.inputTokens + streamTokenUsage.cacheCreationTokens + streamTokenUsage.outputTokens;
|
|
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
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Build budget stats string for GitHub PR comments (Issue #1491)
|
|
245
|
+
* Similar to buildCostInfoString but for token budget statistics
|
|
246
|
+
* @param {Object} tokenUsage - Token usage data from calculateSessionTokens
|
|
247
|
+
* @param {Object|null} streamTokenUsage - Token usage from stream JSON events
|
|
248
|
+
* @returns {string} Formatted markdown string for PR comment
|
|
249
|
+
*/
|
|
250
|
+
export const buildBudgetStatsString = (tokenUsage, streamTokenUsage) => {
|
|
251
|
+
if (!tokenUsage) return '';
|
|
252
|
+
|
|
253
|
+
let stats = '\n\n### 📊 **Token budget statistics:**';
|
|
254
|
+
|
|
255
|
+
// Per-model breakdown
|
|
256
|
+
if (tokenUsage.modelUsage) {
|
|
257
|
+
const modelIds = Object.keys(tokenUsage.modelUsage);
|
|
258
|
+
for (const modelId of modelIds) {
|
|
259
|
+
const usage = tokenUsage.modelUsage[modelId];
|
|
260
|
+
const modelName = usage.modelName || modelId;
|
|
261
|
+
const contextLimit = usage.modelInfo?.limit?.context;
|
|
262
|
+
const outputLimit = usage.modelInfo?.limit?.output;
|
|
263
|
+
const totalInput = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
|
|
264
|
+
|
|
265
|
+
if (modelIds.length > 1) stats += `\n- **${modelName}**:`;
|
|
266
|
+
|
|
267
|
+
if (contextLimit) {
|
|
268
|
+
const contextPct = ((totalInput / contextLimit) * 100).toFixed(2);
|
|
269
|
+
stats += `\n- Context window: ${totalInput.toLocaleString()} / ${contextLimit.toLocaleString()} tokens (${contextPct}%)`;
|
|
270
|
+
} else {
|
|
271
|
+
stats += `\n- Context tokens used: ${totalInput.toLocaleString()}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (outputLimit) {
|
|
275
|
+
const outputPct = ((usage.outputTokens / outputLimit) * 100).toFixed(2);
|
|
276
|
+
stats += `\n- Output tokens: ${usage.outputTokens.toLocaleString()} / ${outputLimit.toLocaleString()} tokens (${outputPct}%)`;
|
|
277
|
+
} else {
|
|
278
|
+
stats += `\n- Output tokens: ${usage.outputTokens.toLocaleString()}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Sub-session breakdown if compactification occurred
|
|
284
|
+
if (tokenUsage.subSessions && tokenUsage.compactifications) {
|
|
285
|
+
stats += `\n- Compactifications: ${tokenUsage.compactifications.length}`;
|
|
286
|
+
for (let i = 0; i < tokenUsage.subSessions.length; i++) {
|
|
287
|
+
const sub = tokenUsage.subSessions[i];
|
|
288
|
+
const totalInput = sub.inputTokens + sub.cacheCreationTokens + sub.cacheReadTokens;
|
|
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`;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Stream vs JSONL comparison
|
|
295
|
+
if (streamTokenUsage) {
|
|
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
|
+
}
|
|
306
|
+
|
|
307
|
+
return stats;
|
|
308
|
+
};
|
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 } from './claude.budget-stats.lib.mjs';
|
|
15
|
+
import { displayBudgetStats, displaySubSessionStats, displayTokenComparison, 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
|
|
@@ -480,91 +480,6 @@ export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) =
|
|
|
480
480
|
}
|
|
481
481
|
return totalCost;
|
|
482
482
|
};
|
|
483
|
-
/**
|
|
484
|
-
* Display detailed model usage information
|
|
485
|
-
* @param {Object} usage - Usage data for a model
|
|
486
|
-
* @param {Function} log - Logging function
|
|
487
|
-
*/
|
|
488
|
-
const displayModelUsage = async (usage, log) => {
|
|
489
|
-
// Show all model characteristics if available
|
|
490
|
-
if (usage.modelInfo) {
|
|
491
|
-
const info = usage.modelInfo;
|
|
492
|
-
const fields = [
|
|
493
|
-
{ label: 'Model ID', value: info.id },
|
|
494
|
-
{ label: 'Provider', value: info.provider || 'Unknown' },
|
|
495
|
-
{ label: 'Context window', value: info.limit?.context ? `${formatNumber(info.limit.context)} tokens` : null },
|
|
496
|
-
{ label: 'Max output', value: info.limit?.output ? `${formatNumber(info.limit.output)} tokens` : null },
|
|
497
|
-
{ label: 'Input modalities', value: info.modalities?.input?.join(', ') || 'N/A' },
|
|
498
|
-
{ label: 'Output modalities', value: info.modalities?.output?.join(', ') || 'N/A' },
|
|
499
|
-
{ label: 'Knowledge cutoff', value: info.knowledge },
|
|
500
|
-
{ label: 'Released', value: info.release_date },
|
|
501
|
-
{
|
|
502
|
-
label: 'Capabilities',
|
|
503
|
-
value: [info.attachment && 'Attachments', info.reasoning && 'Reasoning', info.temperature && 'Temperature', info.tool_call && 'Tool calls'].filter(Boolean).join(', ') || 'N/A',
|
|
504
|
-
},
|
|
505
|
-
{ label: 'Open weights', value: info.open_weights ? 'Yes' : 'No' },
|
|
506
|
-
];
|
|
507
|
-
for (const { label, value } of fields) {
|
|
508
|
-
if (value) await log(` ${label}: ${value}`);
|
|
509
|
-
}
|
|
510
|
-
await log('');
|
|
511
|
-
} else {
|
|
512
|
-
await log(' ⚠️ Model info not available\n');
|
|
513
|
-
}
|
|
514
|
-
// Show usage data
|
|
515
|
-
await log(' Usage:');
|
|
516
|
-
await log(` Input tokens: ${formatNumber(usage.inputTokens)}`);
|
|
517
|
-
if (usage.cacheCreationTokens > 0) {
|
|
518
|
-
await log(` Cache creation tokens: ${formatNumber(usage.cacheCreationTokens)}`);
|
|
519
|
-
}
|
|
520
|
-
if (usage.cacheReadTokens > 0) {
|
|
521
|
-
await log(` Cache read tokens: ${formatNumber(usage.cacheReadTokens)}`);
|
|
522
|
-
}
|
|
523
|
-
await log(` Output tokens: ${formatNumber(usage.outputTokens)}`);
|
|
524
|
-
if (usage.webSearchRequests > 0) {
|
|
525
|
-
await log(` Web search requests: ${usage.webSearchRequests}`);
|
|
526
|
-
}
|
|
527
|
-
// Show detailed cost calculation
|
|
528
|
-
if (usage.costUSD !== null && usage.costUSD !== undefined && usage.costBreakdown) {
|
|
529
|
-
await log('');
|
|
530
|
-
await log(' Cost Calculation (USD):');
|
|
531
|
-
const breakdown = usage.costBreakdown;
|
|
532
|
-
const types = [
|
|
533
|
-
{ key: 'input', label: 'Input' },
|
|
534
|
-
{ key: 'cacheWrite', label: 'Cache write' },
|
|
535
|
-
{ key: 'cacheRead', label: 'Cache read' },
|
|
536
|
-
{ key: 'output', label: 'Output' },
|
|
537
|
-
];
|
|
538
|
-
for (const { key, label } of types) {
|
|
539
|
-
if (breakdown[key].tokens > 0) {
|
|
540
|
-
await log(` ${label}: ${formatNumber(breakdown[key].tokens)} tokens × $${breakdown[key].costPerMillion}/M = $${breakdown[key].cost.toFixed(6)}`);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
await log(' ─────────────────────────────────');
|
|
544
|
-
await log(` Total: $${usage.costUSD.toFixed(6)}`);
|
|
545
|
-
} else if (usage.modelInfo === null) {
|
|
546
|
-
await log('');
|
|
547
|
-
await log(' Cost: Not available (could not fetch pricing)');
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
/**
|
|
551
|
-
* Display cost comparison between public pricing and Anthropic's official cost
|
|
552
|
-
* @param {number|null} publicCost - Public pricing estimate
|
|
553
|
-
* @param {number|null} anthropicCost - Anthropic's official cost
|
|
554
|
-
* @param {Function} log - Logging function
|
|
555
|
-
*/
|
|
556
|
-
const displayCostComparison = async (publicCost, anthropicCost, log) => {
|
|
557
|
-
await log('\n 💰 Cost estimation:');
|
|
558
|
-
await log(` Public pricing estimate: ${publicCost !== null && publicCost !== undefined ? `$${publicCost.toFixed(6)} USD` : 'unknown'}`);
|
|
559
|
-
await log(` Calculated by Anthropic: ${anthropicCost !== null && anthropicCost !== undefined ? `$${anthropicCost.toFixed(6)} USD` : 'unknown'}`);
|
|
560
|
-
if (publicCost !== null && publicCost !== undefined && anthropicCost !== null && anthropicCost !== undefined) {
|
|
561
|
-
const difference = anthropicCost - publicCost;
|
|
562
|
-
const percentDiff = publicCost > 0 ? (difference / publicCost) * 100 : 0;
|
|
563
|
-
await log(` Difference: $${difference.toFixed(6)} (${percentDiff > 0 ? '+' : ''}${percentDiff.toFixed(2)}%)`);
|
|
564
|
-
} else {
|
|
565
|
-
await log(' Difference: unknown');
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
483
|
export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
569
484
|
const os = (await use('os')).default;
|
|
570
485
|
const homeDir = os.homedir();
|
|
@@ -582,6 +497,10 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
582
497
|
}
|
|
583
498
|
// Initialize per-model usage tracking
|
|
584
499
|
const modelUsage = {};
|
|
500
|
+
// Issue #1491: Track sub-sessions between compactification events
|
|
501
|
+
const subSessions = [];
|
|
502
|
+
let currentSubSession = createEmptySubSessionUsage();
|
|
503
|
+
const compactifications = [];
|
|
585
504
|
try {
|
|
586
505
|
// Read the entire file
|
|
587
506
|
const fileContent = await fs.readFile(sessionFile, 'utf8');
|
|
@@ -590,53 +509,39 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
590
509
|
if (!line.trim()) continue;
|
|
591
510
|
try {
|
|
592
511
|
const entry = JSON.parse(line);
|
|
512
|
+
// Issue #1491: Detect compactification boundary events
|
|
513
|
+
if (entry.type === 'system' && entry.subtype === 'compact_boundary') {
|
|
514
|
+
// Save current sub-session and start a new one
|
|
515
|
+
if (currentSubSession.messageCount > 0) {
|
|
516
|
+
subSessions.push(currentSubSession);
|
|
517
|
+
}
|
|
518
|
+
compactifications.push({
|
|
519
|
+
timestamp: entry.timestamp || null,
|
|
520
|
+
preTokens: entry.compactMetadata?.preTokens || null,
|
|
521
|
+
trigger: entry.compactMetadata?.trigger || 'unknown',
|
|
522
|
+
});
|
|
523
|
+
currentSubSession = createEmptySubSessionUsage();
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
593
526
|
if (entry.message && entry.message.usage && entry.message.model) {
|
|
594
|
-
|
|
595
|
-
|
|
527
|
+
accumulateModelUsage(modelUsage, entry);
|
|
528
|
+
// Issue #1491: Also track per-sub-session usage
|
|
596
529
|
const usage = entry.message.usage;
|
|
597
|
-
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
cacheCreation5mTokens: 0,
|
|
603
|
-
cacheCreation1hTokens: 0,
|
|
604
|
-
cacheReadTokens: 0,
|
|
605
|
-
outputTokens: 0,
|
|
606
|
-
webSearchRequests: 0,
|
|
607
|
-
};
|
|
608
|
-
}
|
|
609
|
-
// Add input tokens
|
|
610
|
-
if (usage.input_tokens) {
|
|
611
|
-
modelUsage[model].inputTokens += usage.input_tokens;
|
|
612
|
-
}
|
|
613
|
-
// Add cache creation tokens (total)
|
|
614
|
-
if (usage.cache_creation_input_tokens) {
|
|
615
|
-
modelUsage[model].cacheCreationTokens += usage.cache_creation_input_tokens;
|
|
616
|
-
}
|
|
617
|
-
// Add cache creation tokens breakdown (5m and 1h)
|
|
618
|
-
if (usage.cache_creation) {
|
|
619
|
-
if (usage.cache_creation.ephemeral_5m_input_tokens) {
|
|
620
|
-
modelUsage[model].cacheCreation5mTokens += usage.cache_creation.ephemeral_5m_input_tokens;
|
|
621
|
-
}
|
|
622
|
-
if (usage.cache_creation.ephemeral_1h_input_tokens) {
|
|
623
|
-
modelUsage[model].cacheCreation1hTokens += usage.cache_creation.ephemeral_1h_input_tokens;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
// Add cache read tokens
|
|
627
|
-
if (usage.cache_read_input_tokens) {
|
|
628
|
-
modelUsage[model].cacheReadTokens += usage.cache_read_input_tokens;
|
|
629
|
-
}
|
|
630
|
-
// Add output tokens
|
|
631
|
-
if (usage.output_tokens) {
|
|
632
|
-
modelUsage[model].outputTokens += usage.output_tokens;
|
|
633
|
-
}
|
|
530
|
+
if (usage.input_tokens) currentSubSession.inputTokens += usage.input_tokens;
|
|
531
|
+
if (usage.cache_creation_input_tokens) currentSubSession.cacheCreationTokens += usage.cache_creation_input_tokens;
|
|
532
|
+
if (usage.cache_read_input_tokens) currentSubSession.cacheReadTokens += usage.cache_read_input_tokens;
|
|
533
|
+
if (usage.output_tokens) currentSubSession.outputTokens += usage.output_tokens;
|
|
534
|
+
currentSubSession.messageCount++;
|
|
634
535
|
}
|
|
635
536
|
} catch {
|
|
636
537
|
// Skip lines that aren't valid JSON
|
|
637
538
|
continue;
|
|
638
539
|
}
|
|
639
540
|
}
|
|
541
|
+
// Push the final sub-session
|
|
542
|
+
if (currentSubSession.messageCount > 0) {
|
|
543
|
+
subSessions.push(currentSubSession);
|
|
544
|
+
}
|
|
640
545
|
// If no usage data was found, return null
|
|
641
546
|
if (Object.keys(modelUsage).length === 0) {
|
|
642
547
|
return null;
|
|
@@ -699,6 +604,9 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
699
604
|
outputTokens: totalOutputTokens,
|
|
700
605
|
totalTokens,
|
|
701
606
|
totalCostUSD: hasCostData ? totalCostUSD : null,
|
|
607
|
+
// Issue #1491: Sub-session and compactification data
|
|
608
|
+
subSessions: subSessions.length > 1 ? subSessions : null, // Only include if compactification occurred
|
|
609
|
+
compactifications: compactifications.length > 0 ? compactifications : null,
|
|
702
610
|
};
|
|
703
611
|
} catch (readError) {
|
|
704
612
|
throw new Error(`Failed to read session file: ${readError.message}`);
|
|
@@ -832,6 +740,14 @@ export const executeClaudeCommand = async params => {
|
|
|
832
740
|
let errorDuringExecution = false;
|
|
833
741
|
let resultSummary = null;
|
|
834
742
|
let resultModelUsage = null;
|
|
743
|
+
// Issue #1491: Track token usage from stream JSON events for independent calculation
|
|
744
|
+
const streamTokenUsage = {
|
|
745
|
+
inputTokens: 0,
|
|
746
|
+
cacheCreationTokens: 0,
|
|
747
|
+
cacheReadTokens: 0,
|
|
748
|
+
outputTokens: 0,
|
|
749
|
+
eventCount: 0,
|
|
750
|
+
};
|
|
835
751
|
// Create interactive mode handler if enabled
|
|
836
752
|
let interactiveHandler = null;
|
|
837
753
|
if (argv.interactiveMode && owner && repo && prNumber) {
|
|
@@ -1054,6 +970,15 @@ export const executeClaudeCommand = async params => {
|
|
|
1054
970
|
lastMessage = data.error || JSON.stringify(data);
|
|
1055
971
|
if (lastMessage.includes('Internal server error')) isInternalServerError = true;
|
|
1056
972
|
}
|
|
973
|
+
// Issue #1491: Track token usage from stream events for independent calculation
|
|
974
|
+
if (data.type === 'assistant' && data.message && data.message.usage) {
|
|
975
|
+
const u = data.message.usage;
|
|
976
|
+
if (u.input_tokens) streamTokenUsage.inputTokens += u.input_tokens;
|
|
977
|
+
if (u.cache_creation_input_tokens) streamTokenUsage.cacheCreationTokens += u.cache_creation_input_tokens;
|
|
978
|
+
if (u.cache_read_input_tokens) streamTokenUsage.cacheReadTokens += u.cache_read_input_tokens;
|
|
979
|
+
if (u.output_tokens) streamTokenUsage.outputTokens += u.output_tokens;
|
|
980
|
+
streamTokenUsage.eventCount++;
|
|
981
|
+
}
|
|
1057
982
|
if (data.type === 'assistant' && data.message && data.message.content) {
|
|
1058
983
|
const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
|
|
1059
984
|
for (const item of content) {
|
|
@@ -1336,6 +1261,15 @@ export const executeClaudeCommand = async params => {
|
|
|
1336
1261
|
await displayBudgetStats(usage, log);
|
|
1337
1262
|
}
|
|
1338
1263
|
}
|
|
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
|
+
}
|
|
1339
1273
|
// Show totals if multiple models were used
|
|
1340
1274
|
if (modelIds.length > 1) {
|
|
1341
1275
|
await log('\n 📈 Total across all models:');
|
|
@@ -1381,6 +1315,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1381
1315
|
errorDuringExecution, // Issue #1088: Track if error_during_execution subtype occurred
|
|
1382
1316
|
resultSummary, // Issue #1263: Include result summary for --attach-solution-summary
|
|
1383
1317
|
resultModelUsage, // Issue #1454
|
|
1318
|
+
streamTokenUsage: streamTokenUsage.eventCount > 0 ? streamTokenUsage : null, // Issue #1491
|
|
1384
1319
|
};
|
|
1385
1320
|
} catch (error) {
|
|
1386
1321
|
reportError(error, {
|
package/src/github.lib.mjs
CHANGED
|
@@ -12,8 +12,8 @@ import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
|
|
|
12
12
|
import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
13
13
|
// Import model info helpers (Issue #1225)
|
|
14
14
|
import { getToolDisplayName, getModelInfoForComment } from './models/index.mjs';
|
|
15
|
-
// Re-export for use by other modules
|
|
16
|
-
|
|
15
|
+
export { getToolDisplayName }; // Re-export for use by other modules
|
|
16
|
+
import { buildBudgetStatsString } from './claude.budget-stats.lib.mjs';
|
|
17
17
|
|
|
18
18
|
/** Build cost estimation string for log comments (Issue #1250) */
|
|
19
19
|
const buildCostInfoString = (totalCostUSD, anthropicTotalCostUSD, pricingInfo) => {
|
|
@@ -366,7 +366,9 @@ export async function attachLogToGitHub(options) {
|
|
|
366
366
|
requestedModel = null, // Issue #1225: The --model flag value
|
|
367
367
|
tool = null, // The tool used (claude, agent, opencode, codex)
|
|
368
368
|
resultModelUsage = null, // Issue #1454
|
|
369
|
+
budgetStatsData = null, // Issue #1491: budget stats for comment
|
|
369
370
|
} = options;
|
|
371
|
+
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.streamTokenUsage) : '';
|
|
370
372
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
371
373
|
const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
|
|
372
374
|
try {
|
|
@@ -552,7 +554,7 @@ ${logContent}
|
|
|
552
554
|
// Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
|
|
553
555
|
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
554
556
|
logComment = `## ⚠️ Solution Draft Finished with Errors
|
|
555
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
|
|
557
|
+
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
556
558
|
|
|
557
559
|
> **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
558
560
|
|
|
@@ -568,10 +570,8 @@ ${logContent}
|
|
|
568
570
|
---
|
|
569
571
|
*Now working session is ended, feel free to review and add any feedback on the solution draft.*`;
|
|
570
572
|
} else {
|
|
571
|
-
// Success log format - use helper function for cost info
|
|
572
573
|
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
573
|
-
// Determine title based on session type
|
|
574
|
-
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
574
|
+
// Determine title based on session type (Issue #1152)
|
|
575
575
|
let title = customTitle;
|
|
576
576
|
let sessionNote = '';
|
|
577
577
|
if (sessionType === 'auto-resume') {
|
|
@@ -585,7 +585,7 @@ ${logContent}
|
|
|
585
585
|
sessionNote = '\n\n**Note**: This session was manually resumed using the --resume flag.';
|
|
586
586
|
}
|
|
587
587
|
logComment = `## ${title}
|
|
588
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}${sessionNote}
|
|
588
|
+
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}${sessionNote}
|
|
589
589
|
|
|
590
590
|
<details>
|
|
591
591
|
<summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
@@ -733,7 +733,7 @@ ${errorMessage}
|
|
|
733
733
|
// Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
|
|
734
734
|
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo);
|
|
735
735
|
logUploadComment = `## ⚠️ Solution Draft Finished with Errors
|
|
736
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
|
|
736
|
+
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
737
737
|
|
|
738
738
|
> **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
739
739
|
|
|
@@ -760,7 +760,7 @@ This log file contains the complete execution trace of the AI ${targetType === '
|
|
|
760
760
|
sessionNote = '\n**Note**: This session was manually resumed using the --resume flag.\n';
|
|
761
761
|
}
|
|
762
762
|
logUploadComment = `## ${title}
|
|
763
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${modelInfoString}
|
|
763
|
+
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
764
764
|
${sessionNote}
|
|
765
765
|
### 📎 **Log file uploaded as ${uploadTypeLabel}${chunkInfo}** (${Math.round(logStats.size / 1024)}KB)
|
|
766
766
|
- [View complete solution draft log](${logUrl})
|
package/src/solve.mjs
CHANGED
|
@@ -876,9 +876,10 @@ try {
|
|
|
876
876
|
let anthropicTotalCostUSD = toolResult.anthropicTotalCostUSD;
|
|
877
877
|
let publicPricingEstimate = toolResult.publicPricingEstimate; // Used by agent tool
|
|
878
878
|
let pricingInfo = toolResult.pricingInfo; // Used by agent tool for detailed pricing
|
|
879
|
-
let errorDuringExecution = toolResult.errorDuringExecution || false;
|
|
880
|
-
let resultSummary = toolResult.resultSummary || null;
|
|
881
|
-
let resultModelUsage = toolResult.resultModelUsage || null;
|
|
879
|
+
let errorDuringExecution = toolResult.errorDuringExecution || false;
|
|
880
|
+
let resultSummary = toolResult.resultSummary || null;
|
|
881
|
+
let resultModelUsage = toolResult.resultModelUsage || null;
|
|
882
|
+
let streamTokenUsage = toolResult.streamTokenUsage || null;
|
|
882
883
|
limitReached = toolResult.limitReached;
|
|
883
884
|
cleanupContext.limitReached = limitReached;
|
|
884
885
|
|
|
@@ -1216,7 +1217,7 @@ try {
|
|
|
1216
1217
|
}
|
|
1217
1218
|
|
|
1218
1219
|
// Search for newly created pull requests and comments
|
|
1219
|
-
const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage);
|
|
1220
|
+
const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage);
|
|
1220
1221
|
const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
|
|
1221
1222
|
|
|
1222
1223
|
// Issue #1162: Auto-restart when PR title/description still has placeholder content
|
|
@@ -1263,7 +1264,7 @@ try {
|
|
|
1263
1264
|
await cleanupClaudeFile(tempDir, branchName, null, argv);
|
|
1264
1265
|
|
|
1265
1266
|
// Re-verify results after restart (without auto-restart flag to prevent recursion)
|
|
1266
|
-
const reVerifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, { ...argv, autoRestartOnNonUpdatedPullRequestDescription: false }, shouldAttachLogs, false, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage);
|
|
1267
|
+
const reVerifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, { ...argv, autoRestartOnNonUpdatedPullRequestDescription: false }, shouldAttachLogs, false, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage);
|
|
1267
1268
|
|
|
1268
1269
|
if (reVerifyResult?.prTitleHasPlaceholder || reVerifyResult?.prBodyHasPlaceholder) {
|
|
1269
1270
|
await log('⚠️ PR title/description still not updated after restart');
|
|
@@ -1492,9 +1493,6 @@ try {
|
|
|
1492
1493
|
// drainHandles() inside safeExit() will unref/close these before process.exit().
|
|
1493
1494
|
await logActiveHandles(msg => log(msg));
|
|
1494
1495
|
|
|
1495
|
-
// Issue #1431: safeExit()
|
|
1496
|
-
// (process.stdin ReadStream, undici Socket pool, command-stream ChildProcess,
|
|
1497
|
-
// process.stdout/stderr WriteStreams) so the event loop exits naturally, then
|
|
1498
|
-
// calls process.exit(0) as a deterministic safety net.
|
|
1496
|
+
// Issue #1431: safeExit() unrefs handles so the event loop exits naturally, then calls process.exit(0)
|
|
1499
1497
|
await safeExit(0, 'Process completed');
|
|
1500
1498
|
}
|
|
@@ -494,9 +494,23 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
494
494
|
};
|
|
495
495
|
|
|
496
496
|
// Verify results by searching for new PRs and comments
|
|
497
|
-
export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null, errorDuringExecution = false, sessionType = 'new', resultModelUsage = null) => {
|
|
497
|
+
export const verifyResults = async (owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart = false, sessionId = null, tempDir = null, anthropicTotalCostUSD = null, publicPricingEstimate = null, pricingInfo = null, errorDuringExecution = false, sessionType = 'new', resultModelUsage = null, streamTokenUsage = null) => {
|
|
498
498
|
await log('\n🔍 Searching for created pull requests or comments...');
|
|
499
499
|
|
|
500
|
+
// Issue #1491: Build budget stats data for GitHub comment (computed once, used in both PR and issue paths)
|
|
501
|
+
let budgetStatsData = null;
|
|
502
|
+
if (argv.tokensBudgetStats && sessionId && tempDir) {
|
|
503
|
+
try {
|
|
504
|
+
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
505
|
+
const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
|
|
506
|
+
if (tokenUsage) {
|
|
507
|
+
budgetStatsData = { tokenUsage, streamTokenUsage };
|
|
508
|
+
}
|
|
509
|
+
} catch (budgetError) {
|
|
510
|
+
if (argv.verbose) await log(` ⚠️ Could not calculate budget stats: ${budgetError.message}`, { verbose: true });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
500
514
|
try {
|
|
501
515
|
// Get the current user's GitHub username
|
|
502
516
|
const userResult = await $`gh api user --jq .login`;
|
|
@@ -713,6 +727,8 @@ Fixes ${issueRef}
|
|
|
713
727
|
tool: argv.tool || 'claude',
|
|
714
728
|
// Issue #1454: Pass resultModelUsage for accurate multi-model display
|
|
715
729
|
resultModelUsage,
|
|
730
|
+
// Issue #1491: Pass budget stats for token budget display in comment
|
|
731
|
+
budgetStatsData,
|
|
716
732
|
});
|
|
717
733
|
}
|
|
718
734
|
|
|
@@ -797,6 +813,8 @@ Fixes ${issueRef}
|
|
|
797
813
|
tool: argv.tool || 'claude',
|
|
798
814
|
// Issue #1454: Pass resultModelUsage for accurate multi-model display
|
|
799
815
|
resultModelUsage,
|
|
816
|
+
// Issue #1491: Pass budget stats for token budget display in comment
|
|
817
|
+
budgetStatsData,
|
|
800
818
|
});
|
|
801
819
|
}
|
|
802
820
|
|
package/src/telegram-bot.mjs
CHANGED
|
@@ -558,25 +558,26 @@ function validateGitHubUrl(args, options = {}) {
|
|
|
558
558
|
return { valid: true, parsed, normalizedUrl: url };
|
|
559
559
|
}
|
|
560
560
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
*
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
561
|
+
// Issue #1460/#1497: safeReply - try Markdown first, fall back to plain text on parsing errors
|
|
562
|
+
async function safeReply(ctx, text, options = {}) {
|
|
563
|
+
try {
|
|
564
|
+
return await ctx.reply(text, { parse_mode: 'Markdown', ...options });
|
|
565
|
+
} catch (error) {
|
|
566
|
+
const isParsingError = error.message && (error.message.includes("can't parse entities") || error.message.includes("Can't parse entities") || error.message.includes("can't find end of") || (error.message.includes('Bad Request') && error.message.includes('400')));
|
|
567
|
+
if (!isParsingError) throw error;
|
|
568
|
+
console.error(`[telegram-bot] safeReply: Markdown parsing failed: ${error.message}`);
|
|
569
|
+
console.error(`[telegram-bot] safeReply: Failing message (${Buffer.byteLength(text, 'utf-8')} bytes): ${text}`);
|
|
570
|
+
const plainText = text
|
|
571
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1 ($2)')
|
|
572
|
+
.replace(/\\_/g, '_')
|
|
573
|
+
.replace(/\\\*/g, '*')
|
|
574
|
+
.replace(/\*([^*]+)\*/g, '$1')
|
|
575
|
+
.replace(/`([^`]+)`/g, '$1');
|
|
576
|
+
return await ctx.reply(plainText, { ...options, parse_mode: undefined });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Execute a start-screen command and update the initial message with the result
|
|
580
581
|
async function executeAndUpdateMessage(ctx, startingMessage, commandName, args, infoBlock) {
|
|
581
582
|
const result = await executeStartScreen(commandName, args);
|
|
582
583
|
const { chat, message_id } = startingMessage;
|
|
@@ -914,8 +915,7 @@ async function handleSolveCommand(ctx) {
|
|
|
914
915
|
if (VERBOSE) {
|
|
915
916
|
console.log('[VERBOSE] Multiple GitHub URLs found in replied message');
|
|
916
917
|
}
|
|
917
|
-
await ctx
|
|
918
|
-
parse_mode: 'Markdown',
|
|
918
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(extraction.error)}`, {
|
|
919
919
|
reply_to_message_id: ctx.message.message_id,
|
|
920
920
|
});
|
|
921
921
|
return;
|
|
@@ -931,7 +931,7 @@ async function handleSolveCommand(ctx) {
|
|
|
931
931
|
if (VERBOSE) {
|
|
932
932
|
console.log('[VERBOSE] No GitHub URL found in replied message');
|
|
933
933
|
}
|
|
934
|
-
await ctx
|
|
934
|
+
await safeReply(ctx, '❌ No GitHub issue/PR link found in the replied message.\n\nExample: Reply to a message containing a GitHub issue link with `/solve`\n\nOr with options: `/solve --model opus`', { reply_to_message_id: ctx.message.message_id });
|
|
935
935
|
return;
|
|
936
936
|
}
|
|
937
937
|
}
|
|
@@ -943,7 +943,7 @@ async function handleSolveCommand(ctx) {
|
|
|
943
943
|
errorMsg += `\n\n💡 Did you mean: \`${validation.suggestion}\``;
|
|
944
944
|
}
|
|
945
945
|
errorMsg += '\n\nExample: `/solve https://github.com/owner/repo/issues/123`\n\nOr reply to a message containing a GitHub link with `/solve`';
|
|
946
|
-
await ctx
|
|
946
|
+
await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
|
|
947
947
|
return;
|
|
948
948
|
}
|
|
949
949
|
|
|
@@ -963,19 +963,19 @@ async function handleSolveCommand(ctx) {
|
|
|
963
963
|
// Validate model name with helpful error message (before yargs validation)
|
|
964
964
|
const modelError = validateModelInArgs(args, solveTool);
|
|
965
965
|
if (modelError) {
|
|
966
|
-
await ctx
|
|
966
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(modelError)}`, { reply_to_message_id: ctx.message.message_id });
|
|
967
967
|
return;
|
|
968
968
|
}
|
|
969
969
|
// Issue #1482: Validate --base-branch early to reject URLs and invalid branch names
|
|
970
970
|
const branchError = validateBranchInArgs(args);
|
|
971
971
|
if (branchError) {
|
|
972
|
-
await ctx
|
|
972
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(branchError)}`, { reply_to_message_id: ctx.message.message_id });
|
|
973
973
|
return;
|
|
974
974
|
}
|
|
975
975
|
// Issue #1092: Detect malformed flag patterns like "-- model" (space after --)
|
|
976
976
|
const { malformed, errors: malformedErrors } = detectMalformedFlags(args);
|
|
977
977
|
if (malformed.length > 0) {
|
|
978
|
-
await ctx
|
|
978
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(malformedErrors.join('\n'))}\n\nPlease check your option syntax.`, { reply_to_message_id: ctx.message.message_id });
|
|
979
979
|
return;
|
|
980
980
|
}
|
|
981
981
|
// Validate merged arguments using solve's yargs config
|
|
@@ -994,8 +994,7 @@ async function handleSolveCommand(ctx) {
|
|
|
994
994
|
|
|
995
995
|
testYargs.parse(args);
|
|
996
996
|
} catch (error) {
|
|
997
|
-
await ctx
|
|
998
|
-
parse_mode: 'Markdown',
|
|
997
|
+
await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
|
|
999
998
|
reply_to_message_id: ctx.message.message_id,
|
|
1000
999
|
});
|
|
1001
1000
|
return;
|
|
@@ -1019,7 +1018,7 @@ async function handleSolveCommand(ctx) {
|
|
|
1019
1018
|
const existingItem = solveQueue.findByUrl(normalizedUrl);
|
|
1020
1019
|
if (existingItem) {
|
|
1021
1020
|
const statusText = existingItem.status === 'starting' || existingItem.status === 'started' ? 'being processed' : 'already in the queue';
|
|
1022
|
-
await ctx
|
|
1021
|
+
await safeReply(ctx, `❌ This URL is ${statusText}.\n\nURL: ${escapeMarkdown(normalizedUrl)}\nStatus: ${existingItem.status}\n\n💡 Use /solve\\_queue to check the queue status.`, { reply_to_message_id: ctx.message.message_id });
|
|
1023
1022
|
return;
|
|
1024
1023
|
}
|
|
1025
1024
|
|
|
@@ -1031,18 +1030,18 @@ async function handleSolveCommand(ctx) {
|
|
|
1031
1030
|
// their command cannot be processed (e.g., disk full, server maintenance pending).
|
|
1032
1031
|
// See: https://github.com/link-assistant/hive-mind/issues/1267
|
|
1033
1032
|
if (check.rejected) {
|
|
1034
|
-
await ctx
|
|
1033
|
+
await safeReply(ctx, `❌ Solve command rejected.\n\n${infoBlock}\n\n🚫 Reason: ${escapeMarkdown(check.rejectReason || 'Unknown')}`, { reply_to_message_id: ctx.message.message_id });
|
|
1035
1034
|
return;
|
|
1036
1035
|
}
|
|
1037
1036
|
|
|
1038
1037
|
if (check.canStart && queueStats.queued === 0) {
|
|
1039
|
-
const startingMessage = await ctx
|
|
1038
|
+
const startingMessage = await safeReply(ctx, `🚀 Starting solve command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
|
|
1040
1039
|
await executeAndUpdateMessage(ctx, startingMessage, 'solve', args, infoBlock);
|
|
1041
1040
|
} else {
|
|
1042
1041
|
const queueItem = solveQueue.enqueue({ url: normalizedUrl, args, ctx, requester, infoBlock, tool: solveTool });
|
|
1043
1042
|
let queueMessage = `📋 Solve command queued (position #${queueStats.queued + 1})\n\n${infoBlock}`;
|
|
1044
|
-
if (check.reason) queueMessage += `\n\n⏳ Waiting: ${check.reason}`;
|
|
1045
|
-
const queuedMessage = await ctx
|
|
1043
|
+
if (check.reason) queueMessage += `\n\n⏳ Waiting: ${escapeMarkdown(check.reason)}`;
|
|
1044
|
+
const queuedMessage = await safeReply(ctx, queueMessage, { reply_to_message_id: ctx.message.message_id });
|
|
1046
1045
|
queueItem.messageInfo = { chatId: queuedMessage.chat.id, messageId: queuedMessage.message_id };
|
|
1047
1046
|
if (!solveQueue.executeCallback) solveQueue.executeCallback = createQueueExecuteCallback(executeStartScreen);
|
|
1048
1047
|
}
|
|
@@ -1122,7 +1121,7 @@ async function handleHiveCommand(ctx) {
|
|
|
1122
1121
|
let errorMsg = `❌ ${validation.error}`;
|
|
1123
1122
|
if (validation.suggestion) errorMsg += `\n\n💡 Did you mean: \`${escapeMarkdown(validation.suggestion)}\``;
|
|
1124
1123
|
errorMsg += '\n\nExample: `/hive https://github.com/owner/repo`';
|
|
1125
|
-
await ctx
|
|
1124
|
+
await safeReply(ctx, errorMsg, { reply_to_message_id: ctx.message.message_id });
|
|
1126
1125
|
return;
|
|
1127
1126
|
}
|
|
1128
1127
|
// Normalize issues_list/pulls_list to base repo URL, or use cleaned URL
|
|
@@ -1149,13 +1148,13 @@ async function handleHiveCommand(ctx) {
|
|
|
1149
1148
|
// Validate model name with helpful error message (before yargs validation)
|
|
1150
1149
|
const hiveModelError = validateModelInArgs(args, hiveTool);
|
|
1151
1150
|
if (hiveModelError) {
|
|
1152
|
-
await ctx
|
|
1151
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(hiveModelError)}`, { reply_to_message_id: ctx.message.message_id });
|
|
1153
1152
|
return;
|
|
1154
1153
|
}
|
|
1155
1154
|
// Issue #1482: Validate branch flags early to reject URLs and invalid branch names
|
|
1156
1155
|
const hiveBranchError = validateBranchInArgs(args);
|
|
1157
1156
|
if (hiveBranchError) {
|
|
1158
|
-
await ctx
|
|
1157
|
+
await safeReply(ctx, `❌ ${escapeMarkdown(hiveBranchError)}`, { reply_to_message_id: ctx.message.message_id });
|
|
1159
1158
|
return;
|
|
1160
1159
|
}
|
|
1161
1160
|
|
|
@@ -1175,8 +1174,7 @@ async function handleHiveCommand(ctx) {
|
|
|
1175
1174
|
|
|
1176
1175
|
testYargs.parse(args);
|
|
1177
1176
|
} catch (error) {
|
|
1178
|
-
await ctx
|
|
1179
|
-
parse_mode: 'Markdown',
|
|
1177
|
+
await safeReply(ctx, `❌ Invalid options: ${escapeMarkdown(error.message || String(error))}\n\nUse /help to see available options`, {
|
|
1180
1178
|
reply_to_message_id: ctx.message.message_id,
|
|
1181
1179
|
});
|
|
1182
1180
|
return;
|
|
@@ -1193,7 +1191,7 @@ async function handleHiveCommand(ctx) {
|
|
|
1193
1191
|
infoBlock += `${userOptionsRaw ? '\n' : '\n\n'}🔒 Locked options: ${escapeMarkdown(hiveOverrides.join(' '))}`;
|
|
1194
1192
|
}
|
|
1195
1193
|
|
|
1196
|
-
const startingMessage = await ctx
|
|
1194
|
+
const startingMessage = await safeReply(ctx, `🚀 Starting hive command...\n\n${infoBlock}`, { reply_to_message_id: ctx.message.message_id });
|
|
1197
1195
|
await executeAndUpdateMessage(ctx, startingMessage, 'hive', args, infoBlock);
|
|
1198
1196
|
}
|
|
1199
1197
|
|