@link-assistant/hive-mind 1.40.0 → 1.40.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 +17 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +117 -26
- package/src/claude.lib.mjs +41 -13
- package/src/config.lib.mjs +5 -2
- package/src/github.lib.mjs +1 -1
- package/src/solve.auto-merge.lib.mjs +17 -0
- package/src/solve.results.lib.mjs +1 -1
- package/src/solve.watch.lib.mjs +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.40.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3dbbe9c: fix: improve context, token and cost estimation accuracy for multi-model sessions (#1508)
|
|
8
|
+
- Merge resultModelUsage from Claude Code result JSON into JSONL-based calculations to include sub-agent model tokens (e.g., Haiku) that are missing from JSONL
|
|
9
|
+
- Split token and context usage per model in budget stats PR comments
|
|
10
|
+
- Show per-model cost breakdown in budget stats
|
|
11
|
+
- Fix sub-sessions being duplicated under each model heading in multi-model mode
|
|
12
|
+
- Add verbose diagnostics indicating when token data is sourced from result JSON vs JSONL
|
|
13
|
+
|
|
14
|
+
## 1.40.1
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- 9df62ed: fix: increase activity timeout to 1hr, fix idle tracking, improve graceful kill (#1510)
|
|
19
|
+
|
|
3
20
|
## 1.40.0
|
|
4
21
|
|
|
5
22
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -200,6 +200,49 @@ export const displayBudgetStats = async (usage, tokenUsage, log) => {
|
|
|
200
200
|
await log(` Total output tokens: ${formatNumber(usage.outputTokens)}`);
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Merge resultModelUsage from Claude Code result JSON into JSONL-based modelUsage map.
|
|
205
|
+
* Issue #1508: The JSONL file may miss sub-agent model entries (e.g., Haiku used internally),
|
|
206
|
+
* while resultModelUsage from the success result event has the authoritative per-model breakdown.
|
|
207
|
+
* @param {Object} modelUsage - Map of model ID to accumulated usage from JSONL parsing
|
|
208
|
+
* @param {Object} resultModelUsage - Per-model usage from Claude Code result JSON event
|
|
209
|
+
*/
|
|
210
|
+
export const mergeResultModelUsage = (modelUsage, resultModelUsage) => {
|
|
211
|
+
if (!resultModelUsage || typeof resultModelUsage !== 'object') return;
|
|
212
|
+
for (const [modelId, resultUsage] of Object.entries(resultModelUsage)) {
|
|
213
|
+
if (modelId.startsWith('<') && modelId.endsWith('>')) continue;
|
|
214
|
+
if (!modelUsage[modelId]) {
|
|
215
|
+
modelUsage[modelId] = {
|
|
216
|
+
inputTokens: resultUsage.inputTokens || 0,
|
|
217
|
+
cacheCreationTokens: resultUsage.cacheCreationInputTokens || 0,
|
|
218
|
+
cacheCreation5mTokens: 0,
|
|
219
|
+
cacheCreation1hTokens: 0,
|
|
220
|
+
cacheReadTokens: resultUsage.cacheReadInputTokens || 0,
|
|
221
|
+
outputTokens: resultUsage.outputTokens || 0,
|
|
222
|
+
webSearchRequests: resultUsage.webSearchRequests || 0,
|
|
223
|
+
_sourceResultJson: true,
|
|
224
|
+
};
|
|
225
|
+
if (resultUsage.costUSD != null) {
|
|
226
|
+
modelUsage[modelId]._resultCostUSD = resultUsage.costUSD;
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const jsonlUsage = modelUsage[modelId];
|
|
230
|
+
const jsonlTotal = jsonlUsage.inputTokens + jsonlUsage.cacheCreationTokens + jsonlUsage.cacheReadTokens + jsonlUsage.outputTokens;
|
|
231
|
+
const resultTotal = (resultUsage.inputTokens || 0) + (resultUsage.cacheCreationInputTokens || 0) + (resultUsage.cacheReadInputTokens || 0) + (resultUsage.outputTokens || 0);
|
|
232
|
+
if (resultTotal > jsonlTotal) {
|
|
233
|
+
jsonlUsage.inputTokens = resultUsage.inputTokens || 0;
|
|
234
|
+
jsonlUsage.cacheCreationTokens = resultUsage.cacheCreationInputTokens || 0;
|
|
235
|
+
jsonlUsage.cacheReadTokens = resultUsage.cacheReadInputTokens || 0;
|
|
236
|
+
jsonlUsage.outputTokens = resultUsage.outputTokens || 0;
|
|
237
|
+
jsonlUsage._sourceResultJson = true;
|
|
238
|
+
}
|
|
239
|
+
if (resultUsage.costUSD != null) {
|
|
240
|
+
jsonlUsage._resultCostUSD = resultUsage.costUSD;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
203
246
|
/**
|
|
204
247
|
* Format a token count with K/M suffix for compact display
|
|
205
248
|
* @param {number} tokens - Token count
|
|
@@ -212,9 +255,43 @@ const formatTokensCompact = tokens => {
|
|
|
212
255
|
};
|
|
213
256
|
|
|
214
257
|
/**
|
|
215
|
-
*
|
|
258
|
+
* Format sub-sessions list for budget stats display
|
|
259
|
+
* @param {Array} subSessions - Array of sub-session usage objects
|
|
260
|
+
* @param {number|null} contextLimit - Context window limit for the model
|
|
261
|
+
* @param {number|null} outputLimit - Output token limit for the model
|
|
262
|
+
* @returns {string} Formatted sub-sessions string
|
|
263
|
+
*/
|
|
264
|
+
const formatSubSessionsList = (subSessions, contextLimit, outputLimit) => {
|
|
265
|
+
let result = '\n\nSub sessions (between compact events):';
|
|
266
|
+
for (let i = 0; i < subSessions.length; i++) {
|
|
267
|
+
const sub = subSessions[i];
|
|
268
|
+
const subPeakContext = sub.peakContextUsage || 0;
|
|
269
|
+
const subTotalInput = sub.inputTokens + sub.cacheCreationTokens + sub.cacheReadTokens;
|
|
270
|
+
let line = `\n${i + 1}. `;
|
|
271
|
+
if (contextLimit && subPeakContext > 0) {
|
|
272
|
+
const pct = ((subPeakContext / contextLimit) * 100).toFixed(0);
|
|
273
|
+
line += `${formatTokensCompact(subPeakContext)} / ${formatTokensCompact(contextLimit)} input tokens (${pct}%)`;
|
|
274
|
+
} else {
|
|
275
|
+
line += `${formatTokensCompact(subTotalInput)} input tokens`;
|
|
276
|
+
}
|
|
277
|
+
if (outputLimit) {
|
|
278
|
+
const outPct = ((sub.outputTokens / outputLimit) * 100).toFixed(0);
|
|
279
|
+
line += `; ${formatTokensCompact(sub.outputTokens)} / ${formatTokensCompact(outputLimit)} output tokens (${outPct}%)`;
|
|
280
|
+
} else {
|
|
281
|
+
line += `; ${formatTokensCompact(sub.outputTokens)} output tokens`;
|
|
282
|
+
}
|
|
283
|
+
result += line;
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Build budget stats string for GitHub PR comments (Issue #1491, #1501, #1508)
|
|
216
290
|
* Format requested by user: sub-sessions between compactification events,
|
|
217
291
|
* per-model breakdown, cumulative totals with cached tokens shown separately.
|
|
292
|
+
* Issue #1508: When multiple models are used, token and context usage is now split by model.
|
|
293
|
+
* Sub-sessions are shown as a global section (not duplicated per model) since JSONL
|
|
294
|
+
* sub-session tracking is global across all models.
|
|
218
295
|
* @param {Object} tokenUsage - Token usage data from calculateSessionTokens
|
|
219
296
|
* @param {Object|null} streamTokenUsage - Token usage from stream JSON events (used for comparison, not displayed)
|
|
220
297
|
* @returns {string} Formatted markdown string for PR comment
|
|
@@ -229,6 +306,20 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
229
306
|
const modelIds = Object.keys(tokenUsage.modelUsage);
|
|
230
307
|
const isMultiModel = modelIds.length > 1;
|
|
231
308
|
|
|
309
|
+
// Issue #1508: For multi-model sessions, show sub-sessions once (globally), not per-model
|
|
310
|
+
// Sub-sessions track compactification boundaries which are session-wide, not model-specific
|
|
311
|
+
const subSessions = tokenUsage.subSessions || [];
|
|
312
|
+
const hasMultipleSubSessions = subSessions.length > 1;
|
|
313
|
+
|
|
314
|
+
if (isMultiModel && hasMultipleSubSessions) {
|
|
315
|
+
// Issue #1508: For multi-model sessions, show global sub-sessions once (not per-model),
|
|
316
|
+
// since sub-sessions track compactification boundaries which are session-wide.
|
|
317
|
+
// Per-model context/output limits are shown below under each model heading.
|
|
318
|
+
const primaryModelId = modelIds[0];
|
|
319
|
+
const primaryUsage = tokenUsage.modelUsage[primaryModelId];
|
|
320
|
+
stats += formatSubSessionsList(subSessions, primaryUsage.modelInfo?.limit?.context, primaryUsage.modelInfo?.limit?.output);
|
|
321
|
+
}
|
|
322
|
+
|
|
232
323
|
for (const modelId of modelIds) {
|
|
233
324
|
const usage = tokenUsage.modelUsage[modelId];
|
|
234
325
|
const modelName = usage.modelName || modelId;
|
|
@@ -237,34 +328,29 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
237
328
|
|
|
238
329
|
if (isMultiModel) stats += `\n\n**${modelName}:**`;
|
|
239
330
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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}%)`;
|
|
331
|
+
if (!isMultiModel && hasMultipleSubSessions) {
|
|
332
|
+
// Single-model + multiple sub-sessions: show sub-sessions under that model
|
|
333
|
+
stats += formatSubSessionsList(subSessions, contextLimit, outputLimit);
|
|
334
|
+
} else if (!isMultiModel && !hasMultipleSubSessions) {
|
|
335
|
+
// Single-model + single sub-session: simplified format with context/output limits
|
|
336
|
+
const peakContext = usage.peakContextUsage || 0;
|
|
337
|
+
if (contextLimit) {
|
|
338
|
+
if (peakContext > 0) {
|
|
339
|
+
const pct = ((peakContext / contextLimit) * 100).toFixed(0);
|
|
340
|
+
stats += `\n- Max context window: ${formatTokensCompact(peakContext)} / ${formatTokensCompact(contextLimit)} input tokens (${pct}%)`;
|
|
261
341
|
} else {
|
|
262
|
-
|
|
342
|
+
const totalInput = usage.inputTokens + usage.cacheCreationTokens + usage.cacheReadTokens;
|
|
343
|
+
const pct = ((totalInput / contextLimit) * 100).toFixed(0);
|
|
344
|
+
stats += `\n- Context window: ${formatTokensCompact(totalInput)} / ${formatTokensCompact(contextLimit)} tokens (${pct}%)`;
|
|
263
345
|
}
|
|
264
|
-
|
|
346
|
+
}
|
|
347
|
+
if (outputLimit) {
|
|
348
|
+
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
349
|
+
stats += `\n- Max output tokens: ${formatTokensCompact(usage.outputTokens)} / ${formatTokensCompact(outputLimit)} output tokens (${outPct}%)`;
|
|
265
350
|
}
|
|
266
351
|
} else {
|
|
267
|
-
//
|
|
352
|
+
// Multi-model (single or multiple sub-sessions): show per-model context/output limits
|
|
353
|
+
// Issue #1508: Context window and max output tokens should be split by model
|
|
268
354
|
const peakContext = usage.peakContextUsage || 0;
|
|
269
355
|
if (contextLimit) {
|
|
270
356
|
if (peakContext > 0) {
|
|
@@ -282,12 +368,17 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
282
368
|
}
|
|
283
369
|
}
|
|
284
370
|
|
|
285
|
-
// Cumulative totals: input tokens + cached shown separately
|
|
371
|
+
// Cumulative totals per model: input tokens + cached shown separately
|
|
286
372
|
const totalInputNonCached = usage.inputTokens + usage.cacheCreationTokens;
|
|
287
373
|
const cachedTokens = usage.cacheReadTokens;
|
|
288
374
|
stats += `\n\nTotal input tokens: ${formatTokensCompact(totalInputNonCached)}`;
|
|
289
375
|
if (cachedTokens > 0) stats += ` + ${formatTokensCompact(cachedTokens)} cached`;
|
|
290
376
|
stats += `\nTotal output tokens: ${formatTokensCompact(usage.outputTokens)} output`;
|
|
377
|
+
|
|
378
|
+
// Issue #1508: Show per-model cost when available
|
|
379
|
+
if (usage.costUSD !== null && usage.costUSD !== undefined) {
|
|
380
|
+
stats += `\nCost: $${usage.costUSD.toFixed(6)}`;
|
|
381
|
+
}
|
|
291
382
|
}
|
|
292
383
|
}
|
|
293
384
|
|
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, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison } from './claude.budget-stats.lib.mjs';
|
|
15
|
+
import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage } 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,7 +480,7 @@ export const calculateModelCost = (usage, modelInfo, includeBreakdown = false) =
|
|
|
480
480
|
}
|
|
481
481
|
return totalCost;
|
|
482
482
|
};
|
|
483
|
-
export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
483
|
+
export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsage = null) => {
|
|
484
484
|
const os = (await use('os')).default;
|
|
485
485
|
const homeDir = os.homedir();
|
|
486
486
|
// Construct the path to the session JSONL file
|
|
@@ -576,6 +576,7 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
576
576
|
if (currentSubSession.messageCount > 0) {
|
|
577
577
|
subSessions.push(currentSubSession);
|
|
578
578
|
}
|
|
579
|
+
mergeResultModelUsage(modelUsage, resultModelUsage);
|
|
579
580
|
// If no usage data was found, return null
|
|
580
581
|
if (Object.keys(modelUsage).length === 0) {
|
|
581
582
|
return null;
|
|
@@ -605,7 +606,7 @@ export const calculateSessionTokens = async (sessionId, tempDir) => {
|
|
|
605
606
|
usage.modelName = modelInfo.name || modelId;
|
|
606
607
|
usage.modelInfo = modelInfo; // Store complete model info
|
|
607
608
|
} else {
|
|
608
|
-
usage.costUSD = null;
|
|
609
|
+
usage.costUSD = usage._resultCostUSD ?? null;
|
|
609
610
|
usage.costBreakdown = null;
|
|
610
611
|
usage.modelName = modelId;
|
|
611
612
|
usage.modelInfo = null;
|
|
@@ -853,21 +854,27 @@ export const executeClaudeCommand = async params => {
|
|
|
853
854
|
let lastEventTime = null;
|
|
854
855
|
let activityTimeoutId = null;
|
|
855
856
|
let isActivityTimeout = false;
|
|
857
|
+
// Issue #1510: Separate SIGTERM (graceful) and SIGKILL (force) phases to allow
|
|
858
|
+
// capturing final output from the process during graceful shutdown
|
|
856
859
|
const forceExitOnTimeout = async () => {
|
|
857
860
|
if (forceExitTriggered) return;
|
|
858
861
|
forceExitTriggered = true;
|
|
859
|
-
await log(`⚠️ Stream timeout —
|
|
862
|
+
await log(`⚠️ Stream timeout — sending SIGTERM for graceful shutdown (Issue #1280, #1510)`, { verbose: true });
|
|
860
863
|
try {
|
|
861
864
|
if (execCommand.kill) {
|
|
862
865
|
execCommand.kill('SIGTERM');
|
|
863
|
-
// Issue #1346: Follow up with SIGKILL after
|
|
866
|
+
// Issue #1346/#1510: Follow up with SIGKILL after 5s if still alive
|
|
867
|
+
// Increased from 2s to 5s to give more time for final output capture
|
|
864
868
|
const t = setTimeout(() => {
|
|
865
869
|
try {
|
|
866
|
-
if (!execCommand.result?.code)
|
|
870
|
+
if (!execCommand.result?.code) {
|
|
871
|
+
log(`⚠️ Process did not exit after SIGTERM, sending SIGKILL`, { verbose: true });
|
|
872
|
+
execCommand.kill('SIGKILL');
|
|
873
|
+
}
|
|
867
874
|
} catch {
|
|
868
875
|
/* exited */
|
|
869
876
|
}
|
|
870
|
-
},
|
|
877
|
+
}, 5000);
|
|
871
878
|
t.unref();
|
|
872
879
|
}
|
|
873
880
|
} catch (e) {
|
|
@@ -892,8 +899,8 @@ export const executeClaudeCommand = async params => {
|
|
|
892
899
|
activityTimeoutId = setTimeout(async () => {
|
|
893
900
|
if (!forceExitTriggered && !resultEventReceived) {
|
|
894
901
|
isActivityTimeout = true;
|
|
895
|
-
const idleSeconds = lastEventTime ? Math.round((Date.now() - lastEventTime) / 1000) : 'unknown';
|
|
896
|
-
await log(`\n⚠️ No stream output for ${timeouts.streamActivityMs / 1000}s after previous activity (idle: ${idleSeconds}
|
|
902
|
+
const idleSeconds = lastEventTime ? `${Math.round((Date.now() - lastEventTime) / 1000)}s` : 'unknown';
|
|
903
|
+
await log(`\n⚠️ No stream output for ${timeouts.streamActivityMs / 1000}s after previous activity (idle: ${idleSeconds}) — force-killing (Issue #1472)`, { level: 'warning' });
|
|
897
904
|
await forceExitOnTimeout();
|
|
898
905
|
}
|
|
899
906
|
}, timeouts.streamActivityMs);
|
|
@@ -901,7 +908,8 @@ export const executeClaudeCommand = async params => {
|
|
|
901
908
|
}
|
|
902
909
|
};
|
|
903
910
|
for await (const chunk of execCommand.stream()) {
|
|
904
|
-
|
|
911
|
+
// Issue #1510: Continue processing stream after SIGTERM to capture final output
|
|
912
|
+
// The stream will naturally end when the process exits (SIGTERM) or is force-killed (SIGKILL after 5s)
|
|
905
913
|
if (!firstChunkReceived) {
|
|
906
914
|
// Issue #1472/#1475: Clear startup timeout on first output
|
|
907
915
|
firstChunkReceived = true;
|
|
@@ -922,12 +930,14 @@ export const executeClaudeCommand = async params => {
|
|
|
922
930
|
if (!line.trim()) continue;
|
|
923
931
|
try {
|
|
924
932
|
const data = sanitizeObjectStrings(JSON.parse(line));
|
|
933
|
+
// Issue #1510: Track last event time for all modes (not just interactive)
|
|
934
|
+
// so activity timeout can report accurate idle duration
|
|
935
|
+
lastEventTime = Date.now();
|
|
925
936
|
if (interactiveHandler) {
|
|
926
937
|
if (!interactiveHandler._firstEventLogged) {
|
|
927
938
|
interactiveHandler._firstEventLogged = true;
|
|
928
939
|
await log(`🔌 Interactive mode: First event received (type: ${data.type || 'unknown'}) — stream is active`, { verbose: true });
|
|
929
940
|
}
|
|
930
|
-
lastEventTime = Date.now();
|
|
931
941
|
try {
|
|
932
942
|
await interactiveHandler.processEvent(data);
|
|
933
943
|
} catch (interactiveError) {
|
|
@@ -1193,6 +1203,19 @@ export const executeClaudeCommand = async params => {
|
|
|
1193
1203
|
const retryMode = isStartupTimeout ? ' (fresh start)' : ' (session preserved)';
|
|
1194
1204
|
await log(`\n⚠️ ${errorLabel} detected. Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}${retryMode}${notRetryableHint}...`, { level: 'warning' });
|
|
1195
1205
|
await log(` Error: ${isStartupTimeout ? `No output from Claude CLI within ${timeouts.streamStartupMs / 1000}s` : isActivityTimeout ? `No output for ${timeouts.streamActivityMs / 1000}s after previous activity` : lastMessage.substring(0, 200)}`, { verbose: true });
|
|
1206
|
+
// Issue #1510: Post PR comment when force-killing and auto-resuming so reviewers can follow the session lifecycle
|
|
1207
|
+
if ((isActivityTimeout || isStartupTimeout) && owner && repo && prNumber && $) {
|
|
1208
|
+
try {
|
|
1209
|
+
const timeoutType = isActivityTimeout ? 'activity' : 'startup';
|
|
1210
|
+
const sessionInfo = sessionId ? `\nSession ID: \`${sessionId}\`` : '';
|
|
1211
|
+
const resumeInfo = isStartupTimeout ? 'Session will be restarted (fresh start).' : `Session will be resumed with \`--resume\` (context preserved).`;
|
|
1212
|
+
const commentBody = `## :warning: Session Force-Killed (${timeoutType} timeout)\n\nThe working session was force-killed due to ${timeoutType} timeout (no stream output for ${isActivityTimeout ? timeouts.streamActivityMs / 1000 : timeouts.streamStartupMs / 1000}s).\n\n**Auto-resuming**: Retry ${retryCount + 1}/${maxRetries} in ${delayLabel}. ${resumeInfo}${sessionInfo}\n\n*This is an automated notification — the session will continue automatically.*`;
|
|
1213
|
+
await $`gh pr comment ${prNumber} --repo ${owner}/${repo} --body ${commentBody}`;
|
|
1214
|
+
await log(` Posted force-kill notification to PR #${prNumber}`, { verbose: true });
|
|
1215
|
+
} catch (commentError) {
|
|
1216
|
+
await log(` Warning: Could not post force-kill comment to PR: ${commentError.message}`, { verbose: true });
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1196
1219
|
// Activity timeout preserves session (work was started), startup timeout does not (no session created)
|
|
1197
1220
|
if (!isStartupTimeout && sessionId && !argv.resume) argv.resume = sessionId;
|
|
1198
1221
|
await waitWithCountdown(delay, log);
|
|
@@ -1285,7 +1308,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1285
1308
|
// Calculate and display total token usage from session JSONL file
|
|
1286
1309
|
if (sessionId && tempDir) {
|
|
1287
1310
|
try {
|
|
1288
|
-
const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
|
|
1311
|
+
const tokenUsage = await calculateSessionTokens(sessionId, tempDir, resultModelUsage);
|
|
1289
1312
|
if (tokenUsage) {
|
|
1290
1313
|
// Issue #1501: Log deduplication stats in verbose mode
|
|
1291
1314
|
if (tokenUsage.duplicateEntriesSkipped > 0) {
|
|
@@ -1298,9 +1321,14 @@ export const executeClaudeCommand = async params => {
|
|
|
1298
1321
|
// Display per-model breakdown
|
|
1299
1322
|
if (tokenUsage.modelUsage) {
|
|
1300
1323
|
const modelIds = Object.keys(tokenUsage.modelUsage);
|
|
1324
|
+
const modelsFromResult = modelIds.filter(id => tokenUsage.modelUsage[id]._sourceResultJson);
|
|
1325
|
+
if (modelsFromResult.length > 0) {
|
|
1326
|
+
await log(`📊 Token data supplemented from result JSON for: ${modelsFromResult.join(', ')}`, { verbose: true });
|
|
1327
|
+
}
|
|
1301
1328
|
for (const modelId of modelIds) {
|
|
1302
1329
|
const usage = tokenUsage.modelUsage[modelId];
|
|
1303
|
-
|
|
1330
|
+
const sourceNote = usage._sourceResultJson ? ' (from result JSON)' : '';
|
|
1331
|
+
await log(`\n 📊 ${usage.modelName || modelId}:${sourceNote}`);
|
|
1304
1332
|
await displayModelUsage(usage, log);
|
|
1305
1333
|
// Display budget stats if flag is enabled
|
|
1306
1334
|
if (argv.tokensBudgetStats && usage.modelInfo?.limit) {
|
package/src/config.lib.mjs
CHANGED
|
@@ -63,8 +63,11 @@ export const timeouts = {
|
|
|
63
63
|
// after at least one event was received, the process is considered hung mid-session.
|
|
64
64
|
// This catches the case where Claude CLI starts producing output but then stops (e.g., the
|
|
65
65
|
// original Issue #1472 where CLI was stuck for 4.5h with all output arriving only at CTRL+C).
|
|
66
|
-
//
|
|
67
|
-
|
|
66
|
+
// Issue #1510: Increased from 300000ms (5 min) to 3600000ms (1 hour) because Claude Code can
|
|
67
|
+
// legitimately wait for long-running operations (docker builds, CI polls, large compilations).
|
|
68
|
+
// The 5-minute timeout was force-killing sessions during `sleep 300 && gh run view ...` commands.
|
|
69
|
+
// Default: 3600000ms (1 hour). Set to 0 to disable. Configurable via environment variable.
|
|
70
|
+
streamActivityMs: parseIntWithDefault('HIVE_MIND_STREAM_ACTIVITY_MS', 3600000),
|
|
68
71
|
};
|
|
69
72
|
|
|
70
73
|
// Auto-continue configurations
|
package/src/github.lib.mjs
CHANGED
|
@@ -399,7 +399,7 @@ export async function attachLogToGitHub(options) {
|
|
|
399
399
|
if (totalCostUSD === null && sessionId && tempDir && !errorMessage) {
|
|
400
400
|
try {
|
|
401
401
|
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
402
|
-
const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
|
|
402
|
+
const tokenUsage = await calculateSessionTokens(sessionId, tempDir, resultModelUsage);
|
|
403
403
|
if (tokenUsage) {
|
|
404
404
|
if (tokenUsage.totalCostUSD !== null && tokenUsage.totalCostUSD !== undefined) {
|
|
405
405
|
totalCostUSD = tokenUsage.totalCostUSD;
|
|
@@ -1120,6 +1120,20 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
1120
1120
|
latestAnthropicCost = toolResult.anthropicTotalCostUSD;
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
|
+
// Issue #1508: Compute budget stats for auto-restart-until-mergeable log comment
|
|
1124
|
+
let autoMergeBudgetStatsData = null;
|
|
1125
|
+
if (argv.tokensBudgetStats && latestSessionId && tempDir) {
|
|
1126
|
+
try {
|
|
1127
|
+
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
1128
|
+
const tokenUsage = await calculateSessionTokens(latestSessionId, tempDir, toolResult.resultModelUsage);
|
|
1129
|
+
if (tokenUsage) {
|
|
1130
|
+
autoMergeBudgetStatsData = { tokenUsage, streamTokenUsage: toolResult.streamTokenUsage || null };
|
|
1131
|
+
}
|
|
1132
|
+
} catch (budgetError) {
|
|
1133
|
+
if (argv.verbose) await log(` ⚠️ Could not calculate budget stats: ${budgetError.message}`, { verbose: true });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1123
1137
|
// Attach log if enabled
|
|
1124
1138
|
const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
|
|
1125
1139
|
if (prNumber && shouldAttachLogs) {
|
|
@@ -1149,6 +1163,9 @@ Once the billing issue is resolved, you can re-run the CI checks or push a new c
|
|
|
1149
1163
|
// Issue #1225: Pass model and tool info for PR comments
|
|
1150
1164
|
requestedModel: argv.model,
|
|
1151
1165
|
tool: argv.tool || 'claude',
|
|
1166
|
+
// Issue #1508: Include budget stats (context/token/cost) for auto-restart log
|
|
1167
|
+
resultModelUsage: toolResult.resultModelUsage || null,
|
|
1168
|
+
budgetStatsData: autoMergeBudgetStatsData,
|
|
1152
1169
|
});
|
|
1153
1170
|
await log(formatAligned('', '✅ Session log uploaded to PR', '', 2));
|
|
1154
1171
|
}
|
|
@@ -502,7 +502,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
502
502
|
if (argv.tokensBudgetStats && sessionId && tempDir) {
|
|
503
503
|
try {
|
|
504
504
|
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
505
|
-
const tokenUsage = await calculateSessionTokens(sessionId, tempDir);
|
|
505
|
+
const tokenUsage = await calculateSessionTokens(sessionId, tempDir, resultModelUsage);
|
|
506
506
|
if (tokenUsage) {
|
|
507
507
|
budgetStatsData = { tokenUsage, streamTokenUsage };
|
|
508
508
|
}
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -300,6 +300,8 @@ export const watchForFeedback = async params => {
|
|
|
300
300
|
// Issue #1225: Pass model and tool info for PR comments
|
|
301
301
|
requestedModel: argv.model,
|
|
302
302
|
tool: argv.tool || 'claude',
|
|
303
|
+
// Issue #1508: Pass model usage for failure log (cost info per model)
|
|
304
|
+
resultModelUsage: toolResult.resultModelUsage || null,
|
|
303
305
|
});
|
|
304
306
|
|
|
305
307
|
if (logUploadSuccess) {
|
|
@@ -338,6 +340,20 @@ export const watchForFeedback = async params => {
|
|
|
338
340
|
}
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
// Issue #1508: Compute budget stats for auto-restart log comment
|
|
344
|
+
let autoRestartBudgetStatsData = null;
|
|
345
|
+
if (argv.tokensBudgetStats && latestSessionId && tempDir) {
|
|
346
|
+
try {
|
|
347
|
+
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
348
|
+
const tokenUsage = await calculateSessionTokens(latestSessionId, tempDir, toolResult.resultModelUsage);
|
|
349
|
+
if (tokenUsage) {
|
|
350
|
+
autoRestartBudgetStatsData = { tokenUsage, streamTokenUsage: toolResult.streamTokenUsage || null };
|
|
351
|
+
}
|
|
352
|
+
} catch (budgetError) {
|
|
353
|
+
if (argv.verbose) await log(` ⚠️ Could not calculate budget stats: ${budgetError.message}`, { verbose: true });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
341
357
|
// Issue #1107: Attach log after each auto-restart session with its own cost estimation
|
|
342
358
|
// This ensures each restart has its own log comment instead of one combined log at the end
|
|
343
359
|
const shouldAttachLogs = argv.attachLogs || argv['attach-logs'];
|
|
@@ -369,6 +385,9 @@ export const watchForFeedback = async params => {
|
|
|
369
385
|
// Issue #1225: Pass model and tool info for PR comments
|
|
370
386
|
requestedModel: argv.model,
|
|
371
387
|
tool: argv.tool || 'claude',
|
|
388
|
+
// Issue #1508: Include budget stats (context/token/cost) for auto-restart log
|
|
389
|
+
resultModelUsage: toolResult.resultModelUsage || null,
|
|
390
|
+
budgetStatsData: autoRestartBudgetStatsData,
|
|
372
391
|
});
|
|
373
392
|
|
|
374
393
|
if (logUploadSuccess) {
|