@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/hive-mind",
3
- "version": "1.40.0",
3
+ "version": "1.40.2",
4
4
  "description": "AI-powered issue solver and hive mind for collaborative problem solving",
5
5
  "main": "src/hive.mjs",
6
6
  "type": "module",
@@ -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
- * Build budget stats string for GitHub PR comments (Issue #1491, #1501)
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
- // Sub-session display (Issue #1501: show per sub-session stats)
241
- const subSessions = tokenUsage.subSessions || [];
242
- const hasMultipleSubSessions = subSessions.length > 1;
243
-
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}%)`;
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
- line += `; ${formatTokensCompact(sub.outputTokens)} output tokens`;
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
- stats += line;
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
- // Single sub-session (or no sub-sessions): simplified format
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
 
@@ -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 — forcing exit (Issue #1280)`, { verbose: true });
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 2s if still alive
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) execCommand.kill('SIGKILL');
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
- }, 2000);
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}s) — force-killing (Issue #1472)`, { level: 'warning' });
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
- if (forceExitTriggered) break;
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
- await log(`\n 📊 ${usage.modelName || modelId}:`);
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) {
@@ -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
- // Default: 300000ms (5 minutes). Set to 0 to disable. Configurable via environment variable.
67
- streamActivityMs: parseIntWithDefault('HIVE_MIND_STREAM_ACTIVITY_MS', 300000),
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
@@ -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
  }
@@ -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) {