@link-assistant/hive-mind 2.0.7 → 2.0.8
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 +33 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +16 -16
- package/src/config.lib.mjs +36 -0
- package/src/github.lib.mjs +7 -2
- package/src/models/index.mjs +19 -3
- package/src/solve.execution.lib.mjs +1 -0
- package/src/solve.mjs +8 -8
- package/src/solve.pre-pr-failure-notifier.lib.mjs +1 -0
- package/src/solve.results.lib.mjs +3 -0
- package/src/tool-retry.lib.mjs +60 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 2.0.8
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 072e941: fix(retry): keep the requested `--model` on transient overloads instead of switching to the fallback (#1949)
|
|
8
|
+
|
|
9
|
+
A transient **HTTP 529 "Overloaded"** result used to be classified as a
|
|
10
|
+
model-_capacity_ error (`isCapacity: true`), which made the shared retry helper
|
|
11
|
+
switch the user's requested `--model` to the configured fallback
|
|
12
|
+
(`opus -> opus-4-7`) on every overload. A 529 is a server-wide, transient
|
|
13
|
+
overload — not a signal that the selected model is full — so the run should retry
|
|
14
|
+
the **same** model. The overload branch in `src/tool-retry.lib.mjs` now returns
|
|
15
|
+
`isCapacity: false`; only a genuine "the selected model is at capacity" message
|
|
16
|
+
still triggers a `--model` switch. The fix lives in the shared helper, so every
|
|
17
|
+
tool (claude, codex, gemini, qwen, opencode, agent) inherits it.
|
|
18
|
+
|
|
19
|
+
Per-request fallback is now delegated to Claude Code itself: the claude tool
|
|
20
|
+
forwards `--fallback-model <id>` so overloads fall back _inside_ the CLI while our
|
|
21
|
+
`--model` stays stable.
|
|
22
|
+
|
|
23
|
+
Two display fixes remove the ambiguity that made this hard to diagnose:
|
|
24
|
+
- Warnings now render the resolved model ID alongside the alias, e.g.
|
|
25
|
+
`opus (claude-opus-4-8) -> opus-4-7 (claude-opus-4-7)`, via a new
|
|
26
|
+
`formatModelWithResolvedId` helper.
|
|
27
|
+
- The verbose per-retry "execution context" block now uses a shared
|
|
28
|
+
`logExecutionContext` helper that prints the resolved model actually passed to
|
|
29
|
+
the CLI, replacing a broken `argv.model === 'opus' ? 'opus' : 'sonnet'`
|
|
30
|
+
heuristic that mislabelled every non-`opus` alias as `sonnet`.
|
|
31
|
+
|
|
32
|
+
The PR/issue comment now shows the requested model with its resolved ID and the
|
|
33
|
+
requested thinking level (e.g. `high (~23999 tokens)`) via a new
|
|
34
|
+
`describeRequestedThinking` helper.
|
|
35
|
+
|
|
3
36
|
## 2.0.7
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { buildMcpConfigWithoutPlaywright, ensureClaudePlaywrightMcpServer } from
|
|
|
26
26
|
import { resolveClaudeSessionToolFlags } from './useless-tools.lib.mjs';
|
|
27
27
|
import { ensureClaudeQuietConfig } from './claude-quiet-config.lib.mjs';
|
|
28
28
|
import { fetchModelInfo } from './model-info.lib.mjs';
|
|
29
|
-
import { classifyRetryableError, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
29
|
+
import { classifyRetryableError, logExecutionContext, maybeSwitchToFallbackModel, waitWithCountdown } from './tool-retry.lib.mjs';
|
|
30
30
|
import { resolveSubSessionSize } from './sub-session-size.lib.mjs'; // Issue #1706
|
|
31
31
|
import { withAgentsMdAsClaudeMd } from './agents-md-claude-support.lib.mjs';
|
|
32
32
|
import { deployHandoffSkill } from './handoff-skill.lib.mjs'; // Issue #1877
|
|
@@ -619,18 +619,10 @@ export const executeClaudeCommand = async params => {
|
|
|
619
619
|
await log(`\n${formatAligned('🔄', 'Retry attempt:', `${retryCount}/${retryLimits.maxTransientErrorRetries}`)}`);
|
|
620
620
|
}
|
|
621
621
|
if (argv.verbose) {
|
|
622
|
-
//
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
await
|
|
626
|
-
await log(` Branch: ${branchName}`, { verbose: true });
|
|
627
|
-
await log(` Prompt length: ${prompt.length} chars`, { verbose: true });
|
|
628
|
-
await log(` System prompt length: ${systemPrompt.length} chars`, { verbose: true });
|
|
629
|
-
if (feedbackLines && feedbackLines.length > 0) {
|
|
630
|
-
await log(` Feedback info included: Yes (${feedbackLines.length} lines)`, { verbose: true });
|
|
631
|
-
} else {
|
|
632
|
-
await log(' Feedback info included: No', { verbose: true });
|
|
633
|
-
}
|
|
622
|
+
// Issue #1949: logExecutionContext shows the requested alias with its resolved
|
|
623
|
+
// full ID (e.g. "opus (claude-opus-4-8)"). The old `argv.model === 'opus' ?
|
|
624
|
+
// 'opus' : 'sonnet'` heuristic mislabelled every non-"opus" alias as "sonnet".
|
|
625
|
+
await logExecutionContext({ log, model: argv.model, tool: 'claude', tempDir, branchName, promptLength: prompt.length, systemPromptLength: systemPrompt.length, feedbackLines });
|
|
634
626
|
}
|
|
635
627
|
const resourcesBefore = await getResourceSnapshot();
|
|
636
628
|
await log('📈 System resources before execution:', { verbose: true });
|
|
@@ -704,7 +696,13 @@ export const executeClaudeCommand = async params => {
|
|
|
704
696
|
const resolvedPlanModel = argv.planModel ? mapModelToId(argv.planModel) : undefined; // Issue #1223
|
|
705
697
|
const effectiveModel = resolvedPlanModel ? 'opusplan' : mappedModel;
|
|
706
698
|
const resolvedExecutionModel = resolvedPlanModel ? mappedModel : undefined;
|
|
699
|
+
// Issue #1949: Let Claude Code handle transient overload (529) fallback via its own
|
|
700
|
+
// `--fallback-model` flag (it re-tries the primary each turn) instead of us swapping
|
|
701
|
+
// `--model`. Only for plain `--model` runs (not opusplan) with a distinct fallback.
|
|
702
|
+
const mappedFallbackModel = argv.fallbackModel ? mapModelToId(argv.fallbackModel) : undefined;
|
|
703
|
+
const useClaudeFallbackModel = !resolvedPlanModel && mappedFallbackModel && mappedFallbackModel !== effectiveModel;
|
|
707
704
|
let claudeArgs = `--output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel}`;
|
|
705
|
+
if (useClaudeFallbackModel) claudeArgs += ` --fallback-model ${mappedFallbackModel}`;
|
|
708
706
|
// Declare queuedFeedback for use in catch/finally blocks and return value
|
|
709
707
|
let queuedFeedback = [];
|
|
710
708
|
// Issue #817: When --accept-incomming-comments-as-input is set and we are
|
|
@@ -761,17 +759,19 @@ export const executeClaudeCommand = async params => {
|
|
|
761
759
|
const simpleEscapedSystem = systemPrompt.replace(/"/g, '\\"');
|
|
762
760
|
const mcpDisableArgs = mcpConfigPath ? ['--strict-mcp-config', '--mcp-config', mcpConfigPath] : [];
|
|
763
761
|
const disallowedToolsArgs = disallowedToolsList.length ? ['--disallowedTools', ...disallowedToolsList] : [];
|
|
762
|
+
const fallbackModelArgs = useClaudeFallbackModel ? ['--fallback-model', mappedFallbackModel] : []; // Issue #1949: Claude Code's per-request overload fallback
|
|
763
|
+
if (useClaudeFallbackModel && argv.verbose) await log(`📊 Claude --fallback-model: ${mappedFallbackModel} (Issue #1949 — primary --model ${effectiveModel} stays stable across overload retries)`, { verbose: true });
|
|
764
764
|
if (argv.resume) {
|
|
765
765
|
const simpleEscapedPrompt = prompt.replace(/"/g, '\\"');
|
|
766
|
-
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
766
|
+
execCommand = $({ cwd: tempDir, mirror: false, env: claudeEnv })`${claudePath} --resume ${argv.resume} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} -p "${simpleEscapedPrompt}" --append-system-prompt "${simpleEscapedSystem}"`;
|
|
767
767
|
} else if (streamingInput) {
|
|
768
768
|
// Issue #817: Drive Claude via --input-format stream-json on a pipe
|
|
769
769
|
// stdin. Initial prompt + later PR comments are written as NDJSON
|
|
770
770
|
// frames by attachStreamingInput (see bidirectional-interactive.lib.mjs).
|
|
771
771
|
const streamingInputArgs = ['-p', '--input-format', 'stream-json'];
|
|
772
|
-
execCommand = $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
772
|
+
execCommand = $({ cwd: tempDir, stdin: 'pipe', mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} ${streamingInputArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
773
773
|
} else {
|
|
774
|
-
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
774
|
+
execCommand = $({ cwd: tempDir, stdin: prompt, mirror: false, env: claudeEnv })`${claudePath} --output-format stream-json --verbose --dangerously-skip-permissions --model ${effectiveModel} ${fallbackModelArgs} ${mcpDisableArgs} ${disallowedToolsArgs} --append-system-prompt "${simpleEscapedSystem}"`;
|
|
775
775
|
}
|
|
776
776
|
if (streamingInput) {
|
|
777
777
|
await attachStreamingInput(bidirectionalHandler, execCommand, prompt, log, !!argv.verbose);
|
package/src/config.lib.mjs
CHANGED
|
@@ -412,6 +412,42 @@ export const getTokensToThinkingLevel = (maxBudget = DEFAULT_MAX_THINKING_BUDGET
|
|
|
412
412
|
// Default tokens to thinking level function (using default max budget)
|
|
413
413
|
export const tokensToThinkingLevel = getTokensToThinkingLevel(DEFAULT_MAX_THINKING_BUDGET);
|
|
414
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Issue #1949: Produce a human-readable description of the thinking level that was
|
|
417
|
+
* requested for a run, for inclusion in the PR/issue "Models used" comment. The
|
|
418
|
+
* user asked us to "display requested and (actual thinking level if possible)".
|
|
419
|
+
*
|
|
420
|
+
* The level is derived from `argv.think` (an explicit level keyword) and/or
|
|
421
|
+
* `argv.thinkingBudget` (an explicit token budget), mirroring the same translation
|
|
422
|
+
* logic used by resolveThinkingSettings():
|
|
423
|
+
* - When only --think is given, show the keyword (with the token budget it maps to).
|
|
424
|
+
* - When only --thinking-budget is given, derive the keyword from the budget.
|
|
425
|
+
* - When neither is given, the level is the tool's default → returns null so the
|
|
426
|
+
* caller can omit the line rather than guess.
|
|
427
|
+
*
|
|
428
|
+
* @param {Object} argv - Parsed CLI args (reads think, thinkingBudget, maxThinkingBudget)
|
|
429
|
+
* @returns {string|null} e.g. "high (~24000 tokens)", "off (disabled)", or null
|
|
430
|
+
*/
|
|
431
|
+
export const describeRequestedThinking = (argv = {}) => {
|
|
432
|
+
if (!argv || typeof argv !== 'object') return null;
|
|
433
|
+
const maxBudget = argv.maxThinkingBudget ?? DEFAULT_MAX_THINKING_BUDGET;
|
|
434
|
+
const levelToTokens = getThinkingLevelToTokens(maxBudget);
|
|
435
|
+
const tokensToLevel = getTokensToThinkingLevel(maxBudget);
|
|
436
|
+
|
|
437
|
+
let level = argv.think;
|
|
438
|
+
let budget = argv.thinkingBudget;
|
|
439
|
+
|
|
440
|
+
// Neither specified → tool default; the caller omits the line.
|
|
441
|
+
if (level === undefined && budget === undefined) return null;
|
|
442
|
+
|
|
443
|
+
if (level === undefined && budget !== undefined) level = tokensToLevel(budget);
|
|
444
|
+
if (budget === undefined && level !== undefined) budget = levelToTokens[level];
|
|
445
|
+
|
|
446
|
+
if (level === 'off' || budget === 0) return 'off (disabled)';
|
|
447
|
+
if (budget !== undefined && budget !== null) return `${level} (~${budget} tokens)`;
|
|
448
|
+
return String(level);
|
|
449
|
+
};
|
|
450
|
+
|
|
415
451
|
/**
|
|
416
452
|
* Valid effort levels for Opus 4.6 and Sonnet 4.6 (Issue #1238, Issue #1620)
|
|
417
453
|
* These models use CLAUDE_CODE_EFFORT_LEVEL for thinking depth control
|
package/src/github.lib.mjs
CHANGED
|
@@ -5,7 +5,7 @@ if (typeof globalThis.use === 'undefined') await ensureUseM();
|
|
|
5
5
|
const { $ } = await use('command-stream'); // Use command-stream for consistent $ behavior
|
|
6
6
|
import { log, maskToken, cleanErrorMessage, isENOSPC, ghCmdRetry } from './lib.mjs';
|
|
7
7
|
import { reportError } from './sentry.lib.mjs';
|
|
8
|
-
import { githubLimits, timeouts } from './config.lib.mjs';
|
|
8
|
+
import { describeRequestedThinking, githubLimits, timeouts } from './config.lib.mjs';
|
|
9
9
|
import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRepositories as batchCheckArchived } from './github.batch.lib.mjs';
|
|
10
10
|
import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent } from './token-sanitization.lib.mjs';
|
|
11
11
|
export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent }; // Re-export for backward compatibility
|
|
@@ -356,6 +356,8 @@ export async function attachLogToGitHub(options) {
|
|
|
356
356
|
pricingInfo = null,
|
|
357
357
|
errorDuringExecution = false, // Issue #1088
|
|
358
358
|
requestedModel = null, // Issue #1225: The --model flag value
|
|
359
|
+
argv = null, // Issue #1949: parsed CLI args, used to derive the requested thinking level for the comment
|
|
360
|
+
thinkingInfo = null, // Issue #1949: explicit thinking level description (overrides the value derived from argv)
|
|
359
361
|
tool = null, // The tool used (claude, agent, opencode, codex)
|
|
360
362
|
resultModelUsage = null, // Issue #1454
|
|
361
363
|
budgetStatsData = null, // Issue #1491: budget stats for comment
|
|
@@ -428,7 +430,10 @@ export async function attachLogToGitHub(options) {
|
|
|
428
430
|
let modelInfoString = '';
|
|
429
431
|
if (requestedModel || tool || actualModelIds) {
|
|
430
432
|
try {
|
|
431
|
-
|
|
433
|
+
// Issue #1949: prefer an explicit thinkingInfo, otherwise derive it from argv
|
|
434
|
+
// (e.g. "high (~24000 tokens)"). null when the run used the tool's default.
|
|
435
|
+
const resolvedThinkingInfo = thinkingInfo ?? describeRequestedThinking(argv);
|
|
436
|
+
modelInfoString = await getModelInfoForComment({ requestedModel, tool, pricingInfo, actualModelIds, thinkingInfo: resolvedThinkingInfo });
|
|
432
437
|
if (verbose && modelInfoString) {
|
|
433
438
|
await log(' 🤖 Model info fetched for comment', { verbose: true });
|
|
434
439
|
}
|
package/src/models/index.mjs
CHANGED
|
@@ -926,7 +926,7 @@ const doesRequestedMatchActual = (requestedModel, actualModelId, tool) => {
|
|
|
926
926
|
* @param {Array<{modelId: string, modelInfo: Object|null}>|null} options.modelsUsed - Actual models used from CLI JSON output
|
|
927
927
|
* @returns {string} Formatted markdown string for model info section
|
|
928
928
|
*/
|
|
929
|
-
export const buildModelInfoString = ({ requestedModel = null, tool = null, pricingInfo = null, modelInfo = null, modelsUsed = null } = {}) => {
|
|
929
|
+
export const buildModelInfoString = ({ requestedModel = null, tool = null, pricingInfo = null, modelInfo = null, modelsUsed = null, thinkingInfo = null } = {}) => {
|
|
930
930
|
const hasRequested = requestedModel !== null && requestedModel !== undefined;
|
|
931
931
|
const hasModelsUsed = Array.isArray(modelsUsed) && modelsUsed.length > 0;
|
|
932
932
|
const hasModelInfo = modelInfo !== null;
|
|
@@ -941,7 +941,22 @@ export const buildModelInfoString = ({ requestedModel = null, tool = null, prici
|
|
|
941
941
|
}
|
|
942
942
|
|
|
943
943
|
if (hasRequested) {
|
|
944
|
-
|
|
944
|
+
// Issue #1949: the bare alias (e.g. "opus") is ambiguous \u2014 show the full model
|
|
945
|
+
// ID it resolves to so reviewers know exactly which model ran, e.g.
|
|
946
|
+
// "Requested: `opus` (`claude-opus-4-8`)". When the alias already equals its
|
|
947
|
+
// resolved ID (or cannot be resolved) we just print the alias once.
|
|
948
|
+
const resolvedRequested = resolveModelId(requestedModel, tool);
|
|
949
|
+
if (resolvedRequested && String(resolvedRequested).toLowerCase() !== String(requestedModel).toLowerCase()) {
|
|
950
|
+
info += `\n- Requested: \`${requestedModel}\` (\`${resolvedRequested}\`)`;
|
|
951
|
+
} else {
|
|
952
|
+
info += `\n- Requested: \`${requestedModel}\``;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Issue #1949: surface the requested thinking level alongside the model so the
|
|
957
|
+
// comment records how deeply the model was asked to think (null = tool default).
|
|
958
|
+
if (thinkingInfo) {
|
|
959
|
+
info += `\n- Thinking level: ${thinkingInfo}`;
|
|
945
960
|
}
|
|
946
961
|
|
|
947
962
|
if (hasModelsUsed) {
|
|
@@ -1044,7 +1059,7 @@ export const resolveDefaultFallbackModel = (tool, model) => {
|
|
|
1044
1059
|
* @param {Array<string>|null} options.actualModelIds - Actual model IDs from CLI JSON output
|
|
1045
1060
|
* @returns {Promise<string>} Formatted markdown model info section
|
|
1046
1061
|
*/
|
|
1047
|
-
export const getModelInfoForComment = async ({ requestedModel = null, tool = null, pricingInfo = null, actualModelIds = null } = {}) => {
|
|
1062
|
+
export const getModelInfoForComment = async ({ requestedModel = null, tool = null, pricingInfo = null, actualModelIds = null, thinkingInfo = null } = {}) => {
|
|
1048
1063
|
let modelIds = [];
|
|
1049
1064
|
|
|
1050
1065
|
if (Array.isArray(actualModelIds) && actualModelIds.length > 0) {
|
|
@@ -1075,5 +1090,6 @@ export const getModelInfoForComment = async ({ requestedModel = null, tool = nul
|
|
|
1075
1090
|
pricingInfo,
|
|
1076
1091
|
modelInfo: modelsUsed.length === 0 ? firstModelInfo : null,
|
|
1077
1092
|
modelsUsed: modelsUsed.length > 0 ? modelsUsed : null,
|
|
1093
|
+
thinkingInfo,
|
|
1078
1094
|
});
|
|
1079
1095
|
};
|
|
@@ -197,6 +197,7 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
|
|
|
197
197
|
verbose: argv.verbose || false,
|
|
198
198
|
errorMessage: cleanErrorMessage(error),
|
|
199
199
|
// Issue #1225: Pass model and tool info for PR comments
|
|
200
|
+
argv,
|
|
200
201
|
requestedModel: argv.originalModel || argv.model,
|
|
201
202
|
tool: argv.tool || 'claude',
|
|
202
203
|
});
|
package/src/solve.mjs
CHANGED
|
@@ -942,10 +942,10 @@ try {
|
|
|
942
942
|
toolName: getToolDisplayName(argv.tool),
|
|
943
943
|
resumeCommand,
|
|
944
944
|
sessionId,
|
|
945
|
+
argv,
|
|
945
946
|
requestedModel: argv.originalModel || argv.model,
|
|
946
947
|
tool: argv.tool || 'claude',
|
|
947
|
-
// Issue #1454:
|
|
948
|
-
resultModelUsage,
|
|
948
|
+
resultModelUsage, // Issue #1454: accurate multi-model display
|
|
949
949
|
});
|
|
950
950
|
|
|
951
951
|
if (logUploadSuccess) {
|
|
@@ -1014,10 +1014,10 @@ try {
|
|
|
1014
1014
|
// See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
1015
1015
|
isAutoResumeEnabled: true,
|
|
1016
1016
|
autoResumeMode: limitContinueMode,
|
|
1017
|
+
argv,
|
|
1017
1018
|
requestedModel: argv.originalModel || argv.model,
|
|
1018
1019
|
tool: argv.tool || 'claude',
|
|
1019
|
-
// Issue #1454:
|
|
1020
|
-
resultModelUsage,
|
|
1020
|
+
resultModelUsage, // Issue #1454: accurate multi-model display
|
|
1021
1021
|
});
|
|
1022
1022
|
|
|
1023
1023
|
if (logUploadSuccess) {
|
|
@@ -1125,10 +1125,10 @@ try {
|
|
|
1125
1125
|
sessionId,
|
|
1126
1126
|
// If not a usage limit case, fall back to generic failure format
|
|
1127
1127
|
errorMessage: limitReached ? undefined : toolFailureMessage,
|
|
1128
|
+
argv,
|
|
1128
1129
|
requestedModel: argv.originalModel || argv.model,
|
|
1129
1130
|
tool: argv.tool || 'claude',
|
|
1130
|
-
// Issue #1454:
|
|
1131
|
-
resultModelUsage,
|
|
1131
|
+
resultModelUsage, // Issue #1454: accurate multi-model display
|
|
1132
1132
|
});
|
|
1133
1133
|
|
|
1134
1134
|
if (logUploadSuccess) {
|
|
@@ -1380,10 +1380,10 @@ try {
|
|
|
1380
1380
|
sessionId,
|
|
1381
1381
|
tempDir,
|
|
1382
1382
|
anthropicTotalCostUSD,
|
|
1383
|
+
argv,
|
|
1383
1384
|
requestedModel: argv.originalModel || argv.model,
|
|
1384
1385
|
tool: argv.tool || 'claude',
|
|
1385
|
-
// Issue #1454:
|
|
1386
|
-
resultModelUsage,
|
|
1386
|
+
resultModelUsage, // Issue #1454: accurate multi-model display
|
|
1387
1387
|
});
|
|
1388
1388
|
|
|
1389
1389
|
if (logUploadSuccess) {
|
|
@@ -206,6 +206,7 @@ export async function notifyIssueAboutPrePullRequestFailure(options) {
|
|
|
206
206
|
verbose: argv.verbose,
|
|
207
207
|
errorMessage: `${errorPrefix}\n\nReason: ${reason || 'Unknown error'}`,
|
|
208
208
|
failureActionSection: buildPrePullRequestFailureActionSection(reason),
|
|
209
|
+
argv,
|
|
209
210
|
requestedModel: argv.originalModel || argv.model,
|
|
210
211
|
tool: argv.tool || 'claude',
|
|
211
212
|
});
|
|
@@ -875,6 +875,7 @@ Fixes ${issueRef}
|
|
|
875
875
|
// Issue #1152: Pass sessionType for differentiated log comments
|
|
876
876
|
sessionType,
|
|
877
877
|
// Issue #1225: Pass model and tool info for PR comments
|
|
878
|
+
argv,
|
|
878
879
|
requestedModel: argv.originalModel || argv.model,
|
|
879
880
|
tool: argv.tool || 'claude',
|
|
880
881
|
// Issue #1454: Pass resultModelUsage for accurate multi-model display
|
|
@@ -961,6 +962,7 @@ Fixes ${issueRef}
|
|
|
961
962
|
// Issue #1152: Pass sessionType for differentiated log comments
|
|
962
963
|
sessionType,
|
|
963
964
|
// Issue #1225: Pass model and tool info for issue comments
|
|
965
|
+
argv,
|
|
964
966
|
requestedModel: argv.originalModel || argv.model,
|
|
965
967
|
tool: argv.tool || 'claude',
|
|
966
968
|
// Issue #1454: Pass resultModelUsage for accurate multi-model display
|
|
@@ -1052,6 +1054,7 @@ export const handleExecutionError = async (error, shouldAttachLogs, owner, repo,
|
|
|
1052
1054
|
verbose: argv.verbose || false,
|
|
1053
1055
|
errorMessage: cleanErrorMessage(error),
|
|
1054
1056
|
// Issue #1225: Pass model and tool info for PR comments
|
|
1057
|
+
argv,
|
|
1055
1058
|
requestedModel: argv.originalModel || argv.model,
|
|
1056
1059
|
tool: argv.tool || 'claude',
|
|
1057
1060
|
});
|
package/src/tool-retry.lib.mjs
CHANGED
|
@@ -27,12 +27,28 @@ export const classifyRetryableError = value => {
|
|
|
27
27
|
const message = normalizeMessage(value);
|
|
28
28
|
const lower = message.toLowerCase();
|
|
29
29
|
|
|
30
|
+
// Genuine model-specific capacity: the API explicitly tells us this *particular*
|
|
31
|
+
// model is full and recommends trying a *different* model (e.g. Codex's
|
|
32
|
+
// "Selected model is at capacity. Please try a different model."). Here a model
|
|
33
|
+
// switch is the correct, API-recommended recovery, so isCapacity stays true.
|
|
30
34
|
if (lower.includes('selected model is at capacity') || (lower.includes('at capacity') && lower.includes('try a different model'))) {
|
|
31
35
|
return { message, isRetryable: true, isCapacity: true, label: 'Model capacity error' };
|
|
32
36
|
}
|
|
33
37
|
|
|
38
|
+
// Issue #1949: Transient server-wide overload (HTTP 529 / "overloaded_error"). The
|
|
39
|
+
// Claude API surfaces this as a synthetic result message:
|
|
40
|
+
// "API Error: 529 Overloaded. This is a server-side issue, usually temporary —
|
|
41
|
+
// try again in a moment. If it persists, check https://status.claude.com."
|
|
42
|
+
// This is NOT a model-specific capacity problem — Anthropic's own guidance is to
|
|
43
|
+
// retry the *same* request after a short backoff (the message literally says "try
|
|
44
|
+
// again in a moment"). Switching the requested `--model` to a fallback (e.g.
|
|
45
|
+
// opus -> opus-4-7) is wrong here: it silently downgrades the user's chosen model
|
|
46
|
+
// for a purely transient blip, and the fallback model lives behind the same
|
|
47
|
+
// overloaded API anyway. Claude Code already exposes its own per-request fallback
|
|
48
|
+
// via `--fallback-model` (wired in claude.lib.mjs), so we keep `--model` stable and
|
|
49
|
+
// simply retry. Therefore isCapacity is false → retry with the same model.
|
|
34
50
|
if (lower.includes('overloaded') || lower.includes('overloaded_error')) {
|
|
35
|
-
return { message, isRetryable: true, isCapacity:
|
|
51
|
+
return { message, isRetryable: true, isCapacity: false, label: 'API overload' };
|
|
36
52
|
}
|
|
37
53
|
|
|
38
54
|
if (lower.includes('request timed out')) {
|
|
@@ -140,6 +156,34 @@ export const resolveConfiguredFallbackModel = ({ tool, currentModel, configuredF
|
|
|
140
156
|
return resolveDefaultFallbackModel(tool, currentModel);
|
|
141
157
|
};
|
|
142
158
|
|
|
159
|
+
// Issue #1949: Render a model alias together with the full ID it resolves to, e.g.
|
|
160
|
+
// "opus (claude-opus-4-8)". Earlier the warning printed only the bare alias, so a
|
|
161
|
+
// message like "Switching to fallback model: opus -> opus-4-7" was ambiguous (what
|
|
162
|
+
// does "opus" actually map to?). Showing both removes that ambiguity. When the alias
|
|
163
|
+
// already equals its resolved ID (or cannot be resolved) we just print the alias.
|
|
164
|
+
export const formatModelWithResolvedId = (model, tool) => {
|
|
165
|
+
if (!model) return String(model);
|
|
166
|
+
const resolved = resolveModelId(model, tool);
|
|
167
|
+
if (!resolved || normalizeModelKey(resolved) === normalizeModelKey(model)) return String(model);
|
|
168
|
+
return `${model} (${resolved})`;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Issue #1949: Shared verbose "execution context" logger. Extracted from
|
|
172
|
+
// claude.lib.mjs so the per-tool retry loops can emit a consistent pre-run summary
|
|
173
|
+
// (resolved model, working dir, branch, prompt sizes, feedback) without each file
|
|
174
|
+
// duplicating the block. The model line uses formatModelWithResolvedId so the alias
|
|
175
|
+
// and its full ID are always shown together (e.g. "opus (claude-opus-4-8)").
|
|
176
|
+
export const logExecutionContext = async ({ log, model, tool, tempDir, branchName, promptLength, systemPromptLength, feedbackLines } = {}) => {
|
|
177
|
+
if (typeof log !== 'function') return;
|
|
178
|
+
await log(` Model: ${formatModelWithResolvedId(model, tool)}`, { verbose: true });
|
|
179
|
+
await log(` Working directory: ${tempDir}`, { verbose: true });
|
|
180
|
+
await log(` Branch: ${branchName}`, { verbose: true });
|
|
181
|
+
await log(` Prompt length: ${promptLength} chars`, { verbose: true });
|
|
182
|
+
await log(` System prompt length: ${systemPromptLength} chars`, { verbose: true });
|
|
183
|
+
const feedbackCount = feedbackLines && feedbackLines.length > 0 ? feedbackLines.length : 0;
|
|
184
|
+
await log(feedbackCount > 0 ? ` Feedback info included: Yes (${feedbackCount} lines)` : ' Feedback info included: No', { verbose: true });
|
|
185
|
+
};
|
|
186
|
+
|
|
143
187
|
export const maybeSwitchToFallbackModel = async ({ tool, argv, log, errorMessage } = {}) => {
|
|
144
188
|
const fallbackModel = resolveConfiguredFallbackModel({
|
|
145
189
|
tool,
|
|
@@ -148,7 +192,17 @@ export const maybeSwitchToFallbackModel = async ({ tool, argv, log, errorMessage
|
|
|
148
192
|
});
|
|
149
193
|
|
|
150
194
|
const classification = classifyRetryableError(errorMessage);
|
|
195
|
+
|
|
196
|
+
// Issue #1949: Only switch the requested `--model` for genuine model-specific
|
|
197
|
+
// capacity errors where the API itself recommends a different model. Transient,
|
|
198
|
+
// retryable conditions (overload/529, timeouts, rate limits, socket drops, …) are
|
|
199
|
+
// classified with isCapacity=false and must retry the *same* model — Claude Code's
|
|
200
|
+
// own `--fallback-model` handles per-request fallback for those without us mutating
|
|
201
|
+
// the user's chosen model.
|
|
151
202
|
if (!fallbackModel || !classification.isCapacity || !argv?.model) {
|
|
203
|
+
if (typeof log === 'function' && classification.isRetryable && !classification.isCapacity) {
|
|
204
|
+
await log(` Keeping requested model ${formatModelWithResolvedId(argv?.model, tool)} (transient ${classification.label || 'error'} — no fallback switch, Issue #1949)`, { verbose: true });
|
|
205
|
+
}
|
|
152
206
|
return { switched: false, fallbackModel, reason: classification.label };
|
|
153
207
|
}
|
|
154
208
|
|
|
@@ -163,7 +217,9 @@ export const maybeSwitchToFallbackModel = async ({ tool, argv, log, errorMessage
|
|
|
163
217
|
if (!argv.fallbackModel) argv.fallbackModel = fallbackModel;
|
|
164
218
|
|
|
165
219
|
if (typeof log === 'function') {
|
|
166
|
-
|
|
220
|
+
// Issue #1949: show the resolved full model IDs so the switch is unambiguous,
|
|
221
|
+
// e.g. "opus (claude-opus-4-8) -> opus-4-7 (claude-opus-4-7)".
|
|
222
|
+
await log(`🔀 Switching to fallback model: ${formatModelWithResolvedId(previousModel, tool)} -> ${formatModelWithResolvedId(fallbackModel, tool)}`, { level: 'warning' });
|
|
167
223
|
}
|
|
168
224
|
|
|
169
225
|
return {
|
|
@@ -180,4 +236,6 @@ export default {
|
|
|
180
236
|
waitWithCountdown,
|
|
181
237
|
resolveConfiguredFallbackModel,
|
|
182
238
|
maybeSwitchToFallbackModel,
|
|
239
|
+
formatModelWithResolvedId,
|
|
240
|
+
logExecutionContext,
|
|
183
241
|
};
|