@link-assistant/hive-mind 1.50.4 → 1.50.6
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 +18 -0
- package/package.json +1 -1
- package/src/claude.budget-stats.lib.mjs +169 -4
- package/src/claude.lib.mjs +28 -39
- package/src/claude.stderr.lib.mjs +38 -0
- package/src/github.lib.mjs +1 -1
- package/src/solve.auto-continue.lib.mjs +10 -2
- package/src/solve.auto-merge.lib.mjs +8 -37
- package/src/solve.mjs +12 -11
- package/src/solve.results.lib.mjs +2 -2
- package/src/solve.watch.lib.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.50.6
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 854a74b: feat: track sub-agent calls and show per-call stats in budget display (#1590)
|
|
8
|
+
- Split budget usage statistics per sub-agent call when working sessions contain multiple sub-agent invocations
|
|
9
|
+
- Extract and display individual sub-agent call metrics from Claude API session data
|
|
10
|
+
- Add budget stats library for parsing and formatting per-call usage information
|
|
11
|
+
|
|
12
|
+
## 1.50.5
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- 61b2a32: fix: prevent solution draft log and ready to merge comments from appearing between limit reached and auto resume (#1571)
|
|
17
|
+
- `autoContinueWhenLimitResets()` now awaits child process exit instead of returning immediately after spawn
|
|
18
|
+
- Added defense-in-depth guard in solve.mjs to skip post-processing when limit was reached with auto-continue enabled
|
|
19
|
+
- This ensures the correct comment ordering: Limit Reached → Auto Resume → Solution Draft Log → Ready to merge
|
|
20
|
+
|
|
3
21
|
## 1.50.4
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -362,11 +362,71 @@ const formatContextOutputLine = (peakContext, contextLimit, outputTokens, output
|
|
|
362
362
|
* @param {Object} tokenUsage - Token usage data from calculateSessionTokens or buildAgentBudgetStats
|
|
363
363
|
* @returns {string} Formatted markdown string for PR comment
|
|
364
364
|
*/
|
|
365
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Issue #1590: Build a map of model short name to sub-agent call count.
|
|
367
|
+
* Sub-agent calls use short model names (e.g., "sonnet", "haiku", "opus")
|
|
368
|
+
* while modelUsage uses full model IDs (e.g., "claude-sonnet-4-6").
|
|
369
|
+
* @param {Array|null} subAgentCalls - Array of {id, description, model} from stream tracking
|
|
370
|
+
* @returns {Object} Map of model short name to call count, e.g., {"sonnet": 12, "haiku": 3}
|
|
371
|
+
*/
|
|
372
|
+
const buildSubAgentCallCounts = subAgentCalls => {
|
|
373
|
+
if (!subAgentCalls || subAgentCalls.length === 0) return {};
|
|
374
|
+
const counts = {};
|
|
375
|
+
for (const call of subAgentCalls) {
|
|
376
|
+
const model = call.model || 'default';
|
|
377
|
+
counts[model] = (counts[model] || 0) + 1;
|
|
378
|
+
}
|
|
379
|
+
return counts;
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Issue #1590: Match a full model ID to sub-agent call count.
|
|
384
|
+
* Maps full model IDs (e.g., "claude-sonnet-4-6") to short names used in Agent tool
|
|
385
|
+
* (e.g., "sonnet") and returns the call count.
|
|
386
|
+
* @param {string} modelId - Full model ID
|
|
387
|
+
* @param {Object} callCounts - Map from buildSubAgentCallCounts
|
|
388
|
+
* @returns {number} Number of sub-agent calls for this model, or 0 if none
|
|
389
|
+
*/
|
|
390
|
+
const getSubAgentCallCount = (modelId, callCounts) => {
|
|
391
|
+
if (!callCounts || Object.keys(callCounts).length === 0) return 0;
|
|
392
|
+
// Direct match first (e.g., model short name used as full ID)
|
|
393
|
+
if (callCounts[modelId]) return callCounts[modelId];
|
|
394
|
+
// Match short names to full model IDs:
|
|
395
|
+
// "claude-sonnet-4-6" contains "sonnet", "claude-haiku-4-5-20251001" contains "haiku", etc.
|
|
396
|
+
const modelIdLower = modelId.toLowerCase();
|
|
397
|
+
for (const [shortName, count] of Object.entries(callCounts)) {
|
|
398
|
+
if (modelIdLower.includes(shortName.toLowerCase())) return count;
|
|
399
|
+
}
|
|
400
|
+
return 0;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Issue #1590: Get sub-agent calls matching a specific model ID.
|
|
405
|
+
* Filters the subAgentCalls array to return only calls whose short model name
|
|
406
|
+
* matches the given full model ID.
|
|
407
|
+
* @param {string} modelId - Full model ID (e.g., "claude-sonnet-4-6")
|
|
408
|
+
* @param {Array|null} subAgentCalls - Array of {id, description, model} from stream tracking
|
|
409
|
+
* @returns {Array} Matching sub-agent calls for this model
|
|
410
|
+
*/
|
|
411
|
+
const getSubAgentCallsForModel = (modelId, subAgentCalls) => {
|
|
412
|
+
if (!subAgentCalls || subAgentCalls.length === 0) return [];
|
|
413
|
+
const modelIdLower = modelId.toLowerCase();
|
|
414
|
+
return subAgentCalls.filter(call => {
|
|
415
|
+
const shortName = (call.model || 'default').toLowerCase();
|
|
416
|
+
return modelIdLower === shortName || modelIdLower.includes(shortName);
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
export const buildBudgetStatsString = (tokenUsage, subAgentCalls = null) => {
|
|
366
421
|
if (!tokenUsage) return '';
|
|
367
422
|
|
|
368
423
|
let stats = '\n\n### 📊 **Context and tokens usage:**';
|
|
369
424
|
|
|
425
|
+
// Issue #1590: Build sub-agent call counts per model for per-call breakdown
|
|
426
|
+
// Guard: subAgentCalls must be an array (ignore legacy streamUsage objects passed as second arg)
|
|
427
|
+
const validSubAgentCalls = Array.isArray(subAgentCalls) ? subAgentCalls : null;
|
|
428
|
+
const subAgentCallCounts = buildSubAgentCallCounts(validSubAgentCalls);
|
|
429
|
+
|
|
370
430
|
// Per-model breakdown
|
|
371
431
|
if (tokenUsage.modelUsage) {
|
|
372
432
|
const modelIds = Object.keys(tokenUsage.modelUsage);
|
|
@@ -383,7 +443,17 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
383
443
|
const contextLimit = usage.modelInfo?.limit?.context;
|
|
384
444
|
const outputLimit = usage.modelInfo?.limit?.output;
|
|
385
445
|
|
|
386
|
-
if
|
|
446
|
+
// Issue #1590: Check if this model was used as a sub-agent
|
|
447
|
+
const callCount = getSubAgentCallCount(modelId, subAgentCallCounts);
|
|
448
|
+
|
|
449
|
+
if (isMultiModel) {
|
|
450
|
+
// Issue #1590: Show sub-agent call count alongside model name
|
|
451
|
+
if (callCount > 1) {
|
|
452
|
+
stats += `\n\n**${modelName}:** (${callCount} sub-agent calls)`;
|
|
453
|
+
} else {
|
|
454
|
+
stats += `\n\n**${modelName}:**`;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
387
457
|
|
|
388
458
|
const peakContext = usage.peakContextUsage || 0;
|
|
389
459
|
|
|
@@ -410,9 +480,16 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
410
480
|
}
|
|
411
481
|
|
|
412
482
|
// Issue #1547: Consistent output format — use X / Y (Z%) output tokens when limit known
|
|
483
|
+
// Issue #1590: When multiple sub-agent calls exist, show total output without misleading
|
|
484
|
+
// per-call percentage (e.g., 530% is sum across 12 calls, not a single call)
|
|
413
485
|
if (peakContext === 0 && outputLimit) {
|
|
414
|
-
|
|
415
|
-
|
|
486
|
+
if (callCount > 1) {
|
|
487
|
+
// Show total output without percentage (percentage is misleading for aggregated sub-agent calls)
|
|
488
|
+
totalLine += `, ${formatTokensCompact(usage.outputTokens)} output tokens`;
|
|
489
|
+
} else {
|
|
490
|
+
const outPct = ((usage.outputTokens / outputLimit) * 100).toFixed(0);
|
|
491
|
+
totalLine += `, ${formatTokensCompact(usage.outputTokens)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`;
|
|
492
|
+
}
|
|
416
493
|
} else {
|
|
417
494
|
totalLine += `, ${formatTokensCompact(usage.outputTokens)} output tokens`;
|
|
418
495
|
}
|
|
@@ -422,6 +499,61 @@ export const buildBudgetStatsString = tokenUsage => {
|
|
|
422
499
|
totalLine += `, $${usage.costUSD.toFixed(6)} cost`;
|
|
423
500
|
}
|
|
424
501
|
|
|
502
|
+
// Issue #1590: Show individual sub-agent call list when multiple calls exist
|
|
503
|
+
// Total line appears AFTER the sub-agent calls list (not before)
|
|
504
|
+
if (callCount > 1) {
|
|
505
|
+
const matchingCalls = getSubAgentCallsForModel(modelId, validSubAgentCalls);
|
|
506
|
+
// Issue #1590: Check if actual per-call usage data is available from parent_tool_use_id tracking
|
|
507
|
+
const hasActualUsage = matchingCalls.some(c => c.usage && (c.usage.inputTokens > 0 || c.usage.outputTokens > 0 || c.usage.cacheReadTokens > 0 || c.usage.cacheCreationTokens > 0));
|
|
508
|
+
|
|
509
|
+
stats += `\n\nSub-agent calls:`;
|
|
510
|
+
if (hasActualUsage) {
|
|
511
|
+
// Show actual per-call usage with limits and percentages (same format as sub-sessions)
|
|
512
|
+
for (let i = 0; i < matchingCalls.length; i++) {
|
|
513
|
+
const call = matchingCalls[i];
|
|
514
|
+
const cu = call.usage || {};
|
|
515
|
+
const callInput = (cu.inputTokens || 0) + (cu.cacheCreationTokens || 0) + (cu.cacheReadTokens || 0);
|
|
516
|
+
const callOutput = cu.outputTokens || 0;
|
|
517
|
+
const parts = [];
|
|
518
|
+
if (contextLimit) {
|
|
519
|
+
const pct = ((callInput / contextLimit) * 100).toFixed(0);
|
|
520
|
+
parts.push(`${formatTokensCompact(callInput)} / ${formatTokensCompact(contextLimit)} (${pct}%) input tokens`);
|
|
521
|
+
} else {
|
|
522
|
+
parts.push(`${formatTokensCompact(callInput)} input tokens`);
|
|
523
|
+
}
|
|
524
|
+
if (outputLimit) {
|
|
525
|
+
const outPct = ((callOutput / outputLimit) * 100).toFixed(0);
|
|
526
|
+
parts.push(`${formatTokensCompact(callOutput)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`);
|
|
527
|
+
} else {
|
|
528
|
+
parts.push(`${formatTokensCompact(callOutput)} output tokens`);
|
|
529
|
+
}
|
|
530
|
+
stats += `\n${i + 1}. ${parts.join(', ')}`;
|
|
531
|
+
}
|
|
532
|
+
} else {
|
|
533
|
+
// Fallback: show estimates with limits and percentages when actual per-call data is not available
|
|
534
|
+
const avgInput = Math.round((totalInputNonCached + cachedTokens) / callCount);
|
|
535
|
+
const avgOutput = Math.round(usage.outputTokens / callCount);
|
|
536
|
+
for (let i = 0; i < matchingCalls.length; i++) {
|
|
537
|
+
const parts = [];
|
|
538
|
+
if (contextLimit) {
|
|
539
|
+
const pct = ((avgInput / contextLimit) * 100).toFixed(0);
|
|
540
|
+
parts.push(`~${formatTokensCompact(avgInput)} / ${formatTokensCompact(contextLimit)} (${pct}%) input tokens`);
|
|
541
|
+
} else {
|
|
542
|
+
parts.push(`~${formatTokensCompact(avgInput)} input tokens`);
|
|
543
|
+
}
|
|
544
|
+
if (outputLimit) {
|
|
545
|
+
const outPct = ((avgOutput / outputLimit) * 100).toFixed(0);
|
|
546
|
+
parts.push(`~${formatTokensCompact(avgOutput)} / ${formatTokensCompact(outputLimit)} (${outPct}%) output tokens`);
|
|
547
|
+
} else {
|
|
548
|
+
parts.push(`~${formatTokensCompact(avgOutput)} output tokens`);
|
|
549
|
+
}
|
|
550
|
+
stats += `\n${i + 1}. ${parts.join(', ')}`;
|
|
551
|
+
}
|
|
552
|
+
// Note about estimates only when using fallback
|
|
553
|
+
stats += `\n\n_Per-call values are estimates (total ÷ ${callCount}). Exact per-call breakdown requires [upstream support](https://github.com/anthropics/claude-code/issues/46520)._`;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
425
557
|
stats += `\n\nTotal: ${totalLine}`;
|
|
426
558
|
}
|
|
427
559
|
}
|
|
@@ -468,3 +600,36 @@ export const buildAgentBudgetStats = (tokenUsage, pricingInfo) => {
|
|
|
468
600
|
totalTokens: tokenUsage.inputTokens + (tokenUsage.cacheWriteTokens || 0) + tokenUsage.outputTokens,
|
|
469
601
|
};
|
|
470
602
|
};
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Issue #1590: Creates a fresh sub-agent call entry for tracking per-call token usage
|
|
606
|
+
* @param {Object} item - The tool_use content item from the assistant message
|
|
607
|
+
* @returns {Object} Sub-agent call entry with id, description, model, and empty usage
|
|
608
|
+
*/
|
|
609
|
+
export const createSubAgentCallEntry = item => {
|
|
610
|
+
const agentInput = item.input || {};
|
|
611
|
+
return {
|
|
612
|
+
id: item.id || null,
|
|
613
|
+
description: agentInput.description || null,
|
|
614
|
+
model: agentInput.model || null,
|
|
615
|
+
usage: {
|
|
616
|
+
inputTokens: 0,
|
|
617
|
+
cacheCreationTokens: 0,
|
|
618
|
+
cacheReadTokens: 0,
|
|
619
|
+
outputTokens: 0,
|
|
620
|
+
totalTokens: null, // from task_notification
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Issue #1590: Accumulates token usage from a stream event into a sub-agent call entry
|
|
627
|
+
* @param {Object} callEntry - The sub-agent call entry to accumulate into
|
|
628
|
+
* @param {Object} u - The usage object from the stream event
|
|
629
|
+
*/
|
|
630
|
+
export const accumulateSubAgentUsage = (callEntry, u) => {
|
|
631
|
+
if (u.input_tokens) callEntry.usage.inputTokens += u.input_tokens;
|
|
632
|
+
if (u.cache_creation_input_tokens) callEntry.usage.cacheCreationTokens += u.cache_creation_input_tokens;
|
|
633
|
+
if (u.cache_read_input_tokens) callEntry.usage.cacheReadTokens += u.cache_read_input_tokens;
|
|
634
|
+
if (u.output_tokens) callEntry.usage.outputTokens += u.output_tokens;
|
|
635
|
+
};
|
package/src/claude.lib.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs
|
|
|
13
13
|
import { createInteractiveHandler } from './interactive-mode.lib.mjs';
|
|
14
14
|
import { initProgressMonitoring } from './solve.progress-monitoring.lib.mjs';
|
|
15
15
|
import { sanitizeObjectStrings } from './unicode-sanitization.lib.mjs';
|
|
16
|
-
import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage } from './claude.budget-stats.lib.mjs';
|
|
16
|
+
import { displayBudgetStats, createEmptySubSessionUsage, accumulateModelUsage, displayModelUsage, displayCostComparison, mergeResultModelUsage, createSubAgentCallEntry, accumulateSubAgentUsage } from './claude.budget-stats.lib.mjs';
|
|
17
17
|
import { buildClaudeResumeCommand } from './claude.command-builder.lib.mjs';
|
|
18
18
|
import { handleClaudeRuntimeSwitch } from './claude.runtime-switch.lib.mjs'; // see issue #1141
|
|
19
19
|
import { CLAUDE_MODELS as availableModels } from './models/index.mjs'; // Issue #1221
|
|
@@ -653,44 +653,9 @@ export const calculateSessionTokens = async (sessionId, tempDir, resultModelUsag
|
|
|
653
653
|
throw new Error(`Failed to read session file: ${readError.message}`);
|
|
654
654
|
}
|
|
655
655
|
};
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
* Excludes:
|
|
660
|
-
* - Emoji-prefixed warnings (Issue #477): lines starting with ⚠️ or ⚠
|
|
661
|
-
* - JSON-structured log messages with non-error level (Issue #1337):
|
|
662
|
-
* e.g. {"level":"warn","message":"...failed..."} — the word "failed" is in
|
|
663
|
-
* the message text but the level is "warn", so it is NOT an error.
|
|
664
|
-
* Only JSON lines with level "error" or "fatal" are treated as real errors.
|
|
665
|
-
*
|
|
666
|
-
* @param {string} message - A single trimmed stderr line
|
|
667
|
-
* @returns {boolean} true if the line should count as an error
|
|
668
|
-
*/
|
|
669
|
-
export const isStderrError = message => {
|
|
670
|
-
const trimmed = message.trim();
|
|
671
|
-
if (!trimmed) return false;
|
|
672
|
-
// Detection 1: Emoji-prefixed warnings (Issue #477)
|
|
673
|
-
let isWarning = trimmed.startsWith('⚠️') || trimmed.startsWith('⚠');
|
|
674
|
-
// Detection 2: JSON-structured log messages (Issue #1337)
|
|
675
|
-
if (!isWarning && trimmed.startsWith('{')) {
|
|
676
|
-
try {
|
|
677
|
-
const parsed = JSON.parse(trimmed);
|
|
678
|
-
if (parsed && typeof parsed.level === 'string') {
|
|
679
|
-
const level = parsed.level.toLowerCase();
|
|
680
|
-
// Only "error" and "fatal" levels are real errors.
|
|
681
|
-
if (level !== 'error' && level !== 'fatal') {
|
|
682
|
-
isWarning = true;
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
} catch {
|
|
686
|
-
// Not valid JSON — fall through to keyword matching
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
if (!isWarning && (trimmed.includes('Error:') || trimmed.includes('error') || trimmed.includes('failed') || trimmed.includes('not found'))) {
|
|
690
|
-
return true;
|
|
691
|
-
}
|
|
692
|
-
return false;
|
|
693
|
-
};
|
|
656
|
+
// Extracted to claude.stderr.lib.mjs (Issue #477, #1337)
|
|
657
|
+
import { isStderrError } from './claude.stderr.lib.mjs';
|
|
658
|
+
export { isStderrError };
|
|
694
659
|
export const executeClaudeCommand = async params => {
|
|
695
660
|
const {
|
|
696
661
|
tempDir,
|
|
@@ -777,6 +742,10 @@ export const executeClaudeCommand = async params => {
|
|
|
777
742
|
let errorDuringExecution = false;
|
|
778
743
|
let resultSummary = null;
|
|
779
744
|
let resultModelUsage = null;
|
|
745
|
+
// Issue #1590: Track sub-agent calls (Agent tool invocations) for per-call stats
|
|
746
|
+
const subAgentCalls = [];
|
|
747
|
+
// Issue #1590: Map tool_use_id -> subAgentCalls index for accumulating per-call usage from parent_tool_use_id events
|
|
748
|
+
const subAgentCallsByToolUseId = new Map();
|
|
780
749
|
// Issue #1491: Track token usage from stream JSON events for independent calculation
|
|
781
750
|
const streamTokenUsage = {
|
|
782
751
|
inputTokens: 0,
|
|
@@ -1026,6 +995,18 @@ export const executeClaudeCommand = async params => {
|
|
|
1026
995
|
if (u.cache_read_input_tokens) streamTokenUsage.cacheReadTokens += u.cache_read_input_tokens;
|
|
1027
996
|
if (u.output_tokens) streamTokenUsage.outputTokens += u.output_tokens;
|
|
1028
997
|
streamTokenUsage.eventCount++;
|
|
998
|
+
// Issue #1590: Accumulate per-sub-agent usage from parent_tool_use_id
|
|
999
|
+
if (data.parent_tool_use_id && subAgentCallsByToolUseId.has(data.parent_tool_use_id)) {
|
|
1000
|
+
accumulateSubAgentUsage(subAgentCallsByToolUseId.get(data.parent_tool_use_id), u);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Issue #1590: Capture total_tokens from task_notification (completed sub-agent)
|
|
1004
|
+
if (data.type === 'system' && data.subtype === 'task_notification' && data.status === 'completed' && data.tool_use_id) {
|
|
1005
|
+
const callEntry = subAgentCallsByToolUseId.get(data.tool_use_id);
|
|
1006
|
+
if (callEntry && data.usage && data.usage.total_tokens) {
|
|
1007
|
+
callEntry.usage.totalTokens = data.usage.total_tokens;
|
|
1008
|
+
await log(`🤖 Sub-agent "${callEntry.description || 'unknown'}" completed: ${data.usage.total_tokens} total tokens`, { verbose: true });
|
|
1009
|
+
}
|
|
1029
1010
|
}
|
|
1030
1011
|
if (data.type === 'assistant' && data.message && data.message.content) {
|
|
1031
1012
|
const content = Array.isArray(data.message.content) ? data.message.content : [data.message.content];
|
|
@@ -1054,6 +1035,13 @@ export const executeClaudeCommand = async params => {
|
|
|
1054
1035
|
await log('⏱️ Detected request timeout in assistant message (will retry with --resume)', { verbose: true });
|
|
1055
1036
|
}
|
|
1056
1037
|
}
|
|
1038
|
+
// Issue #1590: Track sub-agent calls (Agent tool invocations) for per-call stats
|
|
1039
|
+
if (item.type === 'tool_use' && item.name === 'Agent') {
|
|
1040
|
+
const callEntry = createSubAgentCallEntry(item);
|
|
1041
|
+
subAgentCalls.push(callEntry);
|
|
1042
|
+
if (item.id) subAgentCallsByToolUseId.set(item.id, callEntry);
|
|
1043
|
+
await log(`🤖 Sub-agent call #${subAgentCalls.length}: "${callEntry.description || 'unknown'}" (model: ${callEntry.model || 'default'})`, { verbose: true });
|
|
1044
|
+
}
|
|
1057
1045
|
}
|
|
1058
1046
|
}
|
|
1059
1047
|
} catch (parseError) {
|
|
@@ -1381,6 +1369,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1381
1369
|
resultSummary, // Issue #1263: Include result summary for --attach-solution-summary
|
|
1382
1370
|
resultModelUsage, // Issue #1454
|
|
1383
1371
|
streamTokenUsage: streamTokenUsage.eventCount > 0 ? streamTokenUsage : null, // Issue #1491
|
|
1372
|
+
subAgentCalls: subAgentCalls.length > 0 ? subAgentCalls : null, // Issue #1590
|
|
1384
1373
|
};
|
|
1385
1374
|
} catch (error) {
|
|
1386
1375
|
reportError(error, {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines whether a stderr message line should be treated as an error.
|
|
3
|
+
*
|
|
4
|
+
* Excludes:
|
|
5
|
+
* - Emoji-prefixed warnings (Issue #477): lines starting with ⚠️ or ⚠
|
|
6
|
+
* - JSON-structured log messages with non-error level (Issue #1337):
|
|
7
|
+
* e.g. {"level":"warn","message":"...failed..."} — the word "failed" is in
|
|
8
|
+
* the message text but the level is "warn", so it is NOT an error.
|
|
9
|
+
* Only JSON lines with level "error" or "fatal" are treated as real errors.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} message - A single trimmed stderr line
|
|
12
|
+
* @returns {boolean} true if the line should count as an error
|
|
13
|
+
*/
|
|
14
|
+
export const isStderrError = message => {
|
|
15
|
+
const trimmed = message.trim();
|
|
16
|
+
if (!trimmed) return false;
|
|
17
|
+
// Detection 1: Emoji-prefixed warnings (Issue #477)
|
|
18
|
+
let isWarning = trimmed.startsWith('⚠️') || trimmed.startsWith('⚠');
|
|
19
|
+
// Detection 2: JSON-structured log messages (Issue #1337)
|
|
20
|
+
if (!isWarning && trimmed.startsWith('{')) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(trimmed);
|
|
23
|
+
if (parsed && typeof parsed.level === 'string') {
|
|
24
|
+
const level = parsed.level.toLowerCase();
|
|
25
|
+
// Only "error" and "fatal" levels are real errors.
|
|
26
|
+
if (level !== 'error' && level !== 'fatal') {
|
|
27
|
+
isWarning = true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Not valid JSON — fall through to keyword matching
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!isWarning && (trimmed.includes('Error:') || trimmed.includes('error') || trimmed.includes('failed') || trimmed.includes('not found'))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
};
|
package/src/github.lib.mjs
CHANGED
|
@@ -366,7 +366,7 @@ export async function attachLogToGitHub(options) {
|
|
|
366
366
|
resultModelUsage = null, // Issue #1454
|
|
367
367
|
budgetStatsData = null, // Issue #1491: budget stats for comment
|
|
368
368
|
} = options;
|
|
369
|
-
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage) : '';
|
|
369
|
+
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
|
|
370
370
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
371
371
|
const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
|
|
372
372
|
try {
|
|
@@ -182,8 +182,16 @@ export const autoContinueWhenLimitResets = async (issueUrl, sessionId, argv, sho
|
|
|
182
182
|
env: process.env,
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
child
|
|
186
|
-
|
|
185
|
+
// Issue #1571: Await child process exit to prevent parent from continuing
|
|
186
|
+
// to post "Solution Draft Log" and "Ready to merge" comments before the
|
|
187
|
+
// resumed session starts. Without this await, the parent process would
|
|
188
|
+
// return from this function and continue executing verifyResults() and
|
|
189
|
+
// startAutoRestartUntilMergeable(), causing confusing comment ordering.
|
|
190
|
+
await new Promise(resolve => {
|
|
191
|
+
child.on('close', code => {
|
|
192
|
+
process.exit(code);
|
|
193
|
+
resolve(); // Won't be reached due to process.exit, but included for completeness
|
|
194
|
+
});
|
|
187
195
|
});
|
|
188
196
|
} catch (error) {
|
|
189
197
|
reportError(error, {
|
|
@@ -62,10 +62,7 @@ export const watchUntilMergeable = async params => {
|
|
|
62
62
|
const { issueUrl, owner, repo, issueNumber, prNumber, prBranch, branchName, tempDir, argv } = params;
|
|
63
63
|
|
|
64
64
|
const rawWatchInterval = argv.watchInterval || 60; // seconds
|
|
65
|
-
// Issue #
|
|
66
|
-
// Issue #1567: Reduced from 5 minutes (300s) to 2 minutes (120s) to decrease wait times
|
|
67
|
-
// between working session finish and "Ready to merge" / next action detection.
|
|
68
|
-
// This also applies uniformly whether CI/CD is configured or not.
|
|
65
|
+
// Issue #1567: Minimum 120s interval to conserve API rate limits while keeping responsiveness
|
|
69
66
|
const MIN_CI_CHECK_INTERVAL_SECONDS = 120;
|
|
70
67
|
const watchInterval = Math.max(rawWatchInterval, MIN_CI_CHECK_INTERVAL_SECONDS);
|
|
71
68
|
const isAutoMerge = argv.autoMerge || false;
|
|
@@ -76,39 +73,19 @@ export const watchUntilMergeable = async params => {
|
|
|
76
73
|
let latestSessionId = null;
|
|
77
74
|
let latestAnthropicCost = null;
|
|
78
75
|
|
|
79
|
-
// Issue #1323: Track actual
|
|
80
|
-
// `iteration` counts check cycles (how many times we check for blockers)
|
|
81
|
-
// `restartCount` counts actual AI tool executions (when we actually restart the AI)
|
|
76
|
+
// Issue #1323: Track actual AI restarts separately from check cycle iterations
|
|
82
77
|
let restartCount = 0;
|
|
83
78
|
|
|
84
|
-
// Issue #1371:
|
|
85
|
-
// This replaces the all-time history check (checkForExistingComment) which incorrectly
|
|
86
|
-
// suppressed new notifications when a previous solve run had already posted one.
|
|
87
|
-
// In-memory deduplication correctly handles the case where multiple check cycles in
|
|
88
|
-
// the same run detect mergeability simultaneously, without blocking fresh runs.
|
|
79
|
+
// Issue #1371: In-memory dedup for "Ready to merge" comment (per-session, not all-time)
|
|
89
80
|
let readyToMergeCommentPosted = false;
|
|
90
81
|
|
|
91
82
|
let currentBackoffSeconds = watchInterval;
|
|
92
83
|
|
|
93
|
-
// Issue #1503: Track consecutive "no workflow runs" checks per-SHA
|
|
94
|
-
// The `checkCount` parameter in getMergeBlockers is a safety valve that triggers after
|
|
95
|
-
// MAX_NO_RUNS_CHECKS (5) consecutive checks with zero workflow runs, concluding CI was
|
|
96
|
-
// genuinely not triggered (paths-ignore, fork PRs, etc.). Previously, `iteration` (total
|
|
97
|
-
// loop count) was passed as `checkCount`, which meant after 5 iterations (regardless of
|
|
98
|
-
// CI state), any new push would immediately trigger the safety valve because checkCount
|
|
99
|
-
// was already >= 5. This caused false positive "Ready to merge" when a new commit was
|
|
100
|
-
// pushed and CI hadn't registered yet.
|
|
101
|
-
//
|
|
102
|
-
// Fix: Track the HEAD SHA and reset the counter when it changes (new push detected).
|
|
84
|
+
// Issue #1503: Track consecutive "no workflow runs" checks per-SHA (reset on new push)
|
|
103
85
|
let consecutiveNoRunsChecks = 0;
|
|
104
86
|
let lastKnownHeadSha = null;
|
|
105
87
|
|
|
106
|
-
// Issue #1567: Initial cooldown
|
|
107
|
-
// Wait at least MIN_CI_CHECK_INTERVAL_SECONDS after working session finishes before
|
|
108
|
-
// starting to check. This ensures:
|
|
109
|
-
// 1. Solution Draft Log is fully posted before any "Ready to merge" can appear
|
|
110
|
-
// 2. CI/CD checks have time to register with GitHub (avoids false "no CI" detection)
|
|
111
|
-
// 3. Consistent behavior whether CI/CD is configured or not
|
|
88
|
+
// Issue #1567: Initial cooldown to let CI register and solution logs post
|
|
112
89
|
const INITIAL_COOLDOWN_SECONDS = MIN_CI_CHECK_INTERVAL_SECONDS;
|
|
113
90
|
|
|
114
91
|
await log('');
|
|
@@ -161,9 +138,7 @@ export const watchUntilMergeable = async params => {
|
|
|
161
138
|
await log(formatAligned('🔍', `Check #${iteration}:`, currentTime.toLocaleTimeString()));
|
|
162
139
|
|
|
163
140
|
try {
|
|
164
|
-
// Issue #1503: Get
|
|
165
|
-
// consecutive no-runs counter. This prevents false positives where the counter
|
|
166
|
-
// from a previous commit's checks carries over to a new commit.
|
|
141
|
+
// Issue #1503: Get current HEAD SHA to detect new pushes and reset no-runs counter
|
|
167
142
|
let currentHeadSha = null;
|
|
168
143
|
try {
|
|
169
144
|
const shaResult = await $`gh pr view ${prNumber} --repo ${owner}/${repo} --json headRefOid --jq .headRefOid`;
|
|
@@ -184,17 +159,13 @@ export const watchUntilMergeable = async params => {
|
|
|
184
159
|
readyToMergeCommentPosted = false;
|
|
185
160
|
}
|
|
186
161
|
|
|
187
|
-
// Issue #1503: Increment counter; getMergeBlockers
|
|
188
|
-
// If getMergeBlockers sees no workflow runs on this check, the counter stays incremented.
|
|
189
|
-
// If it sees workflow runs or checks, the counter is irrelevant (different code paths).
|
|
162
|
+
// Issue #1503: Increment counter; getMergeBlockers uses it as a safety valve
|
|
190
163
|
consecutiveNoRunsChecks++;
|
|
191
164
|
|
|
192
165
|
// Get merge blockers
|
|
193
166
|
const { blockers, noCiConfigured, noCiTriggered, workflowRunConclusions, ciStatus } = await getMergeBlockers(owner, repo, prNumber, argv.verbose, consecutiveNoRunsChecks, prBranch);
|
|
194
167
|
|
|
195
|
-
// Issue #1503: Reset
|
|
196
|
-
// This ensures the safety valve only fires after truly consecutive "no runs" checks,
|
|
197
|
-
// not after interleaved pending/success/failure states that happened to reach the count.
|
|
168
|
+
// Issue #1503: Reset counter when CI checks exist (safety valve only for consecutive "no runs")
|
|
198
169
|
if (ciStatus && ciStatus.status !== 'no_checks') {
|
|
199
170
|
// CI checks exist (pending, success, failure, etc.) — the "no runs" counter is irrelevant
|
|
200
171
|
consecutiveNoRunsChecks = 0;
|
package/src/solve.mjs
CHANGED
|
@@ -879,6 +879,7 @@ try {
|
|
|
879
879
|
let resultSummary = toolResult.resultSummary || null;
|
|
880
880
|
let resultModelUsage = toolResult.resultModelUsage || null;
|
|
881
881
|
let streamTokenUsage = toolResult.streamTokenUsage || null;
|
|
882
|
+
let subAgentCalls = toolResult.subAgentCalls || null; // Issue #1590
|
|
882
883
|
limitReached = toolResult.limitReached;
|
|
883
884
|
cleanupContext.limitReached = limitReached;
|
|
884
885
|
|
|
@@ -1077,13 +1078,10 @@ try {
|
|
|
1077
1078
|
}
|
|
1078
1079
|
}
|
|
1079
1080
|
|
|
1080
|
-
//
|
|
1081
|
-
// This allows the code to continue to showSessionSummary() where autoContinueWhenLimitResets() is called
|
|
1081
|
+
// Skip failure exit if limit reached with auto-resume (continues to showSessionSummary/autoContinueWhenLimitResets)
|
|
1082
1082
|
const shouldSkipFailureExitForAutoLimitContinue = limitReached && argv.autoResumeOnLimitReset;
|
|
1083
|
-
|
|
1084
1083
|
if (!success && !shouldSkipFailureExitForAutoLimitContinue) {
|
|
1085
1084
|
// Show claude resume command only for --tool claude (or default) on failure
|
|
1086
|
-
// Uses the (cd ... && claude --resume ...) pattern for a fully copyable, executable command
|
|
1087
1085
|
const toolForFailure = argv.tool || 'claude';
|
|
1088
1086
|
if (sessionId && toolForFailure === 'claude') {
|
|
1089
1087
|
const claudeResumeCmd = buildClaudeResumeCommand({ tempDir, sessionId, model: argv.model });
|
|
@@ -1094,9 +1092,7 @@ try {
|
|
|
1094
1092
|
await log('');
|
|
1095
1093
|
}
|
|
1096
1094
|
|
|
1097
|
-
//
|
|
1098
|
-
// Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
|
|
1099
|
-
// Issues #1212, #1462: Fall back to uploading logs to the issue if PR is not available
|
|
1095
|
+
// Attach failure logs before exiting (Issues #1212, #1462: fall back to issue if no PR)
|
|
1100
1096
|
const hasPR = global.createdPR && global.createdPR.number;
|
|
1101
1097
|
const hasIssue = global.issueNumber;
|
|
1102
1098
|
const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
|
|
@@ -1148,8 +1144,7 @@ try {
|
|
|
1148
1144
|
await safeExit(1, `${argv.tool.toUpperCase()} execution failed`);
|
|
1149
1145
|
}
|
|
1150
1146
|
|
|
1151
|
-
// Clean up .playwright-mcp/
|
|
1152
|
-
// This prevents browser automation artifacts from triggering auto-restart (Issue #1124)
|
|
1147
|
+
// Clean up .playwright-mcp/ to prevent browser artifacts from triggering auto-restart (Issue #1124)
|
|
1153
1148
|
if (argv.playwrightMcpAutoCleanup !== false) {
|
|
1154
1149
|
const playwrightMcpDir = path.join(tempDir, '.playwright-mcp');
|
|
1155
1150
|
try {
|
|
@@ -1179,6 +1174,12 @@ try {
|
|
|
1179
1174
|
// Show summary of session and log file
|
|
1180
1175
|
await showSessionSummary(sessionId, limitReached, argv, issueUrl, tempDir, shouldAttachLogs);
|
|
1181
1176
|
|
|
1177
|
+
// Issue #1571: Defense-in-depth guard — skip post-processing if auto-continue is handling it
|
|
1178
|
+
// (prevents "Solution Draft Log" / "Ready to merge" comments before "Auto Resume")
|
|
1179
|
+
if (limitReached && (argv.autoResumeOnLimitReset || argv.autoRestartOnLimitReset) && global.limitResetTime) {
|
|
1180
|
+
await safeExit(0, 'Auto-continue child process will handle post-processing');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1182
1183
|
// Issue #1263: Handle solution summary attachment
|
|
1183
1184
|
// --attach-solution-summary: Always attach if result summary is available
|
|
1184
1185
|
// --auto-attach-solution-summary: Only attach if AI didn't create any comments during session
|
|
@@ -1215,7 +1216,7 @@ try {
|
|
|
1215
1216
|
}
|
|
1216
1217
|
|
|
1217
1218
|
// Search for newly created pull requests and comments
|
|
1218
|
-
const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage);
|
|
1219
|
+
const verifyResult = await verifyResults(owner, repo, branchName, issueNumber, prNumber, prUrl, referenceTime, argv, shouldAttachLogs, shouldRestart, sessionId, tempDir, anthropicTotalCostUSD, publicPricingEstimate, pricingInfo, errorDuringExecution, sessionType, resultModelUsage, streamTokenUsage, subAgentCalls);
|
|
1219
1220
|
const logsAlreadyUploaded = verifyResult?.logUploadSuccess || false;
|
|
1220
1221
|
|
|
1221
1222
|
// Issue #1162: Auto-restart when PR title/description still has placeholder content
|
|
@@ -1262,7 +1263,7 @@ try {
|
|
|
1262
1263
|
await cleanupClaudeFile(tempDir, branchName, null, argv);
|
|
1263
1264
|
|
|
1264
1265
|
// Re-verify results after restart (without auto-restart flag to prevent recursion)
|
|
1265
|
-
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);
|
|
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, streamTokenUsage, subAgentCalls);
|
|
1266
1267
|
|
|
1267
1268
|
if (reVerifyResult?.prTitleHasPlaceholder || reVerifyResult?.prBodyHasPlaceholder) {
|
|
1268
1269
|
await log('⚠️ PR title/description still not updated after restart');
|
|
@@ -503,7 +503,7 @@ export const showSessionSummary = async (sessionId, limitReached, argv, issueUrl
|
|
|
503
503
|
};
|
|
504
504
|
|
|
505
505
|
// Verify results by searching for new PRs and comments
|
|
506
|
-
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) => {
|
|
506
|
+
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, subAgentCalls = null) => {
|
|
507
507
|
await log('\n🔍 Searching for created pull requests or comments...');
|
|
508
508
|
|
|
509
509
|
// Issue #1491, #1526: Build budget stats data for GitHub comment (computed once, used in both PR and issue paths)
|
|
@@ -513,7 +513,7 @@ export const verifyResults = async (owner, repo, branchName, issueNumber, prNumb
|
|
|
513
513
|
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
514
514
|
const tokenUsage = await calculateSessionTokens(sessionId, tempDir, resultModelUsage);
|
|
515
515
|
if (tokenUsage) {
|
|
516
|
-
budgetStatsData = { tokenUsage, streamTokenUsage };
|
|
516
|
+
budgetStatsData = { tokenUsage, streamTokenUsage, subAgentCalls };
|
|
517
517
|
}
|
|
518
518
|
} catch (budgetError) {
|
|
519
519
|
if (argv.verbose) await log(` ⚠️ Could not calculate budget stats: ${budgetError.message}`, { verbose: true });
|
package/src/solve.watch.lib.mjs
CHANGED
|
@@ -347,7 +347,7 @@ export const watchForFeedback = async params => {
|
|
|
347
347
|
const { calculateSessionTokens } = await import('./claude.lib.mjs');
|
|
348
348
|
const tokenUsage = await calculateSessionTokens(latestSessionId, tempDir, toolResult.resultModelUsage);
|
|
349
349
|
if (tokenUsage) {
|
|
350
|
-
autoRestartBudgetStatsData = { tokenUsage, streamTokenUsage: toolResult.streamTokenUsage || null };
|
|
350
|
+
autoRestartBudgetStatsData = { tokenUsage, streamTokenUsage: toolResult.streamTokenUsage || null, subAgentCalls: toolResult.subAgentCalls || null };
|
|
351
351
|
}
|
|
352
352
|
} catch (budgetError) {
|
|
353
353
|
if (argv.verbose) await log(` ⚠️ Could not calculate budget stats: ${budgetError.message}`, { verbose: true });
|