@link-assistant/hive-mind 1.37.0 → 1.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/claude.lib.mjs +11 -11
- package/src/github.lib.mjs +16 -12
- package/src/lib.mjs +23 -0
- package/src/solve.error-handlers.lib.mjs +1 -1
- package/src/solve.mjs +14 -9
- package/src/solve.repository.lib.mjs +32 -29
- package/src/token-sanitization.lib.mjs +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/hive-mind
|
|
2
2
|
|
|
3
|
+
## 1.37.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8df5a3d: Treat ENOSPC as immediate failure at all stages (issues #1212, #1211)
|
|
8
|
+
|
|
9
|
+
When disk space runs out during any stage — including git clone, execution, and log
|
|
10
|
+
upload — ENOSPC is now treated as a hard failure (not partial success). Added ENOSPC
|
|
11
|
+
detection to git clone error classification so disk-full clone failures are not
|
|
12
|
+
retried. The isENOSPC utility now detects git-specific patterns like "unable to write
|
|
13
|
+
file" and "cannot create directory". Actionable disk cleanup guidance is provided.
|
|
14
|
+
|
|
3
15
|
## 1.37.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/package.json
CHANGED
package/src/claude.lib.mjs
CHANGED
|
@@ -6,7 +6,7 @@ if (typeof globalThis.use === 'undefined') {
|
|
|
6
6
|
const { $ } = await use('command-stream');
|
|
7
7
|
const fs = (await use('fs')).promises;
|
|
8
8
|
const path = (await use('path')).default;
|
|
9
|
-
import { log } from './lib.mjs';
|
|
9
|
+
import { log, isENOSPC } from './lib.mjs';
|
|
10
10
|
import { reportError } from './sentry.lib.mjs';
|
|
11
11
|
import { timeouts, retryLimits, claudeCode, getClaudeEnv, getThinkingLevelToTokens, getTokensToThinkingLevel, supportsThinkingBudget, DEFAULT_MAX_THINKING_BUDGET, getMaxOutputTokensForModel } from './config.lib.mjs';
|
|
12
12
|
import { detectUsageLimit, formatUsageLimitMessage } from './usage-limit.lib.mjs';
|
|
@@ -808,12 +808,10 @@ export const executeClaudeCommand = async params => {
|
|
|
808
808
|
await log(' Feedback info included: No', { verbose: true });
|
|
809
809
|
}
|
|
810
810
|
}
|
|
811
|
-
// Take resource snapshot before execution
|
|
812
811
|
const resourcesBefore = await getResourceSnapshot();
|
|
813
812
|
await log('📈 System resources before execution:', { verbose: true });
|
|
814
813
|
await log(` Memory: ${resourcesBefore.memory.split('\n')[1]}`, { verbose: true });
|
|
815
814
|
await log(` Load: ${resourcesBefore.load}`, { verbose: true });
|
|
816
|
-
// Use command-stream's async iteration for real-time streaming with file logging
|
|
817
815
|
let commandFailed = false;
|
|
818
816
|
let sessionId = null;
|
|
819
817
|
let limitReached = false;
|
|
@@ -1027,7 +1025,12 @@ export const executeClaudeCommand = async params => {
|
|
|
1027
1025
|
const subtype = data.subtype || 'unknown';
|
|
1028
1026
|
if (subtype === 'error_during_execution') {
|
|
1029
1027
|
errorDuringExecution = true;
|
|
1030
|
-
|
|
1028
|
+
if ((data.errors || []).some(e => isENOSPC(e))) {
|
|
1029
|
+
commandFailed = true;
|
|
1030
|
+
await log('❌ ENOSPC: No space left on device. Free disk space (check ~/.claude/debug).');
|
|
1031
|
+
} else {
|
|
1032
|
+
await log(`⚠️ Error during execution (subtype: ${subtype}) - work may be completed`, { verbose: true });
|
|
1033
|
+
}
|
|
1031
1034
|
} else {
|
|
1032
1035
|
commandFailed = true;
|
|
1033
1036
|
await log(`⚠️ Detected error from Claude CLI (subtype: ${subtype})`, { verbose: true });
|
|
@@ -1039,8 +1042,8 @@ export const executeClaudeCommand = async params => {
|
|
|
1039
1042
|
if (lastMessage.includes('Internal server error') && !lastMessage.includes('Overloaded')) {
|
|
1040
1043
|
isInternalServerError = true;
|
|
1041
1044
|
}
|
|
1042
|
-
// Issue #1353: Detect "Request timed out"
|
|
1043
|
-
if (lastMessage
|
|
1045
|
+
// Issue #1353: Detect "Request timed out" from Claude CLI
|
|
1046
|
+
if (lastMessage.includes('Request timed out')) {
|
|
1044
1047
|
isRequestTimeout = true;
|
|
1045
1048
|
await log('⏱️ Detected request timeout from Claude CLI (will retry with --resume)', { verbose: true });
|
|
1046
1049
|
}
|
|
@@ -1277,8 +1280,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1277
1280
|
}
|
|
1278
1281
|
}
|
|
1279
1282
|
}
|
|
1280
|
-
// Issue #1354: Detect silent failures (no messages + stderr errors,
|
|
1281
|
-
// Skip if result event confirmed success (definitive proof regardless of messageCount).
|
|
1283
|
+
// Issue #1354: Detect silent failures (no messages + stderr errors, skip if result confirmed success)
|
|
1282
1284
|
if (!commandFailed && !resultSuccessReceived && stderrErrors.length > 0 && messageCount === 0 && toolUseCount === 0) {
|
|
1283
1285
|
commandFailed = true;
|
|
1284
1286
|
const errorsPreview = stderrErrors
|
|
@@ -1307,9 +1309,7 @@ export const executeClaudeCommand = async params => {
|
|
|
1307
1309
|
resultSummary, // Issue #1263: Include result summary
|
|
1308
1310
|
};
|
|
1309
1311
|
}
|
|
1310
|
-
// Issue #1088:
|
|
1311
|
-
// log it as "Finished with errors" instead of pure success
|
|
1312
|
-
// Issue #1351: Distinguish interrupted sessions (exit code 130) from normal completion
|
|
1312
|
+
// Issue #1088/#1351: Log execution result status
|
|
1313
1313
|
if (exitCode === 130) {
|
|
1314
1314
|
await log('\n\n⚠️ Claude command interrupted (CTRL+C)');
|
|
1315
1315
|
} else if (errorDuringExecution) {
|
package/src/github.lib.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// GitHub-related utility functions. Check if use is already defined (when imported from solve.mjs), if not, fetch it (when running standalone)
|
|
3
3
|
if (typeof globalThis.use === 'undefined') globalThis.use = (await eval(await (await fetch('https://unpkg.com/use-m/use.js')).text())).use;
|
|
4
4
|
const { $ } = await use('command-stream'); // Use command-stream for consistent $ behavior
|
|
5
|
-
import { log, maskToken, cleanErrorMessage } from './lib.mjs';
|
|
5
|
+
import { log, maskToken, cleanErrorMessage, isENOSPC } from './lib.mjs';
|
|
6
6
|
import { reportError } from './sentry.lib.mjs';
|
|
7
7
|
import { githubLimits, timeouts } from './config.lib.mjs';
|
|
8
8
|
import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRepositories as batchCheckArchived } from './github.batch.lib.mjs';
|
|
@@ -162,16 +162,7 @@ export const checkGitHubPermissions = async () => {
|
|
|
162
162
|
return true; // Continue despite permission check failure
|
|
163
163
|
}
|
|
164
164
|
};
|
|
165
|
-
/**
|
|
166
|
-
* Check if the current user has write (push) permissions to a specific repository
|
|
167
|
-
* This helps fail early before wasting AI tokens when --fork option is not used
|
|
168
|
-
* @param {string} owner - Repository owner
|
|
169
|
-
* @param {string} repo - Repository name
|
|
170
|
-
* @param {Object} options - Configuration options
|
|
171
|
-
* @param {boolean} options.useFork - Whether --fork flag is enabled
|
|
172
|
-
* @param {string} options.issueUrl - Original issue URL for error messages
|
|
173
|
-
* @returns {Promise<boolean>} True if has write access OR fork mode is enabled, false otherwise
|
|
174
|
-
*/
|
|
165
|
+
/** Check if user has write permissions to repo. Fails early if --fork not used. */
|
|
175
166
|
export const checkRepositoryWritePermission = async (owner, repo, options = {}) => {
|
|
176
167
|
const { useFork = false, issueUrl = '' } = options;
|
|
177
168
|
// Skip check if fork mode is enabled - user will work in their own fork
|
|
@@ -379,6 +370,17 @@ export async function attachLogToGitHub(options) {
|
|
|
379
370
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
380
371
|
const ghCommand = targetType === 'pr' ? 'pr' : 'issue';
|
|
381
372
|
try {
|
|
373
|
+
// Issue #1212: Check disk space before attempting log upload (100MB minimum)
|
|
374
|
+
try {
|
|
375
|
+
const { checkDiskSpace } = await import('./memory-check.mjs');
|
|
376
|
+
const diskCheck = await checkDiskSpace(100, { log: async () => {} });
|
|
377
|
+
if (!diskCheck.success) {
|
|
378
|
+
await log(` ❌ Insufficient disk space for log upload (${diskCheck.availableMB}MB available, 100MB required). Free disk space and retry.`);
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
/* disk check failure is non-fatal — continue to actual operation */
|
|
383
|
+
}
|
|
382
384
|
// Check if log file exists and is not empty
|
|
383
385
|
const logStats = await fs.stat(logFile);
|
|
384
386
|
if (logStats.size === 0) {
|
|
@@ -800,7 +802,9 @@ ${sessionNote}
|
|
|
800
802
|
return await attachRegularComment(options, logComment);
|
|
801
803
|
}
|
|
802
804
|
} catch (uploadError) {
|
|
803
|
-
|
|
805
|
+
// Issue #1212: ENOSPC-specific actionable guidance
|
|
806
|
+
const msg = isENOSPC(uploadError) ? 'ENOSPC: No space left on device during log upload. Free disk space and retry.' : `Error uploading log file: ${uploadError.message}`;
|
|
807
|
+
await log(` ❌ ${msg}`);
|
|
804
808
|
return false;
|
|
805
809
|
}
|
|
806
810
|
}
|
package/src/lib.mjs
CHANGED
|
@@ -340,6 +340,28 @@ export const measureTime = async (fn, label = 'Operation') => {
|
|
|
340
340
|
}
|
|
341
341
|
};
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Check if an error is an ENOSPC (no space left on device) error
|
|
345
|
+
* Issue #1212: ENOSPC errors need specific handling because they cascade
|
|
346
|
+
* (once disk is full, all operations fail) and require user action (cleanup).
|
|
347
|
+
* @param {Error|string} error - Error object or message
|
|
348
|
+
* @returns {boolean} True if the error is an ENOSPC error
|
|
349
|
+
*/
|
|
350
|
+
export const isENOSPC = error => {
|
|
351
|
+
if (!error) return false;
|
|
352
|
+
const message = error?.message || (typeof error === 'string' ? error : '');
|
|
353
|
+
const lowerMessage = message.toLowerCase();
|
|
354
|
+
return (
|
|
355
|
+
error?.code === 'ENOSPC' ||
|
|
356
|
+
message.includes('ENOSPC') ||
|
|
357
|
+
lowerMessage.includes('no space left on device') ||
|
|
358
|
+
// Issue #1211: git clone ENOSPC patterns — "unable to write file" and
|
|
359
|
+
// "cannot create directory" occur when disk fills during checkout
|
|
360
|
+
(lowerMessage.includes('unable to write file') && lowerMessage.includes('error')) ||
|
|
361
|
+
(lowerMessage.includes('cannot create directory') && lowerMessage.includes('no space left'))
|
|
362
|
+
);
|
|
363
|
+
};
|
|
364
|
+
|
|
343
365
|
/**
|
|
344
366
|
* Clean up error messages for better user experience
|
|
345
367
|
* @param {Error|string} error - Error object or message
|
|
@@ -502,6 +524,7 @@ export default {
|
|
|
502
524
|
retry,
|
|
503
525
|
formatBytes,
|
|
504
526
|
measureTime,
|
|
527
|
+
isENOSPC,
|
|
505
528
|
cleanErrorMessage,
|
|
506
529
|
formatAligned,
|
|
507
530
|
displayFormattedError,
|
|
@@ -43,7 +43,7 @@ export const handleFailure = async options => {
|
|
|
43
43
|
|
|
44
44
|
// If --attach-logs is enabled, try to attach failure logs
|
|
45
45
|
if (shouldAttachLogs && getLogFile()) {
|
|
46
|
-
//
|
|
46
|
+
// Issues #1212, #1462: Upload logs to PR if available, otherwise fall back to the issue
|
|
47
47
|
const hasPR = global.createdPR && global.createdPR.number;
|
|
48
48
|
const hasIssue = global.issueNumber;
|
|
49
49
|
const targetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
|
package/src/solve.mjs
CHANGED
|
@@ -508,8 +508,7 @@ if (isPrUrl) {
|
|
|
508
508
|
issueNumber = urlNumber;
|
|
509
509
|
await log(`📝 Issue mode: Working with issue #${issueNumber}`);
|
|
510
510
|
}
|
|
511
|
-
//
|
|
512
|
-
// as a fallback when PR creation fails and global.createdPR is not available
|
|
511
|
+
// Issues #1212, #1462: Store issueNumber globally for error handlers (attach failure logs to issue when no PR exists)
|
|
513
512
|
global.issueNumber = issueNumber;
|
|
514
513
|
const workspaceInfo = argv.enableWorkspaces ? { owner, repo, issueNumber } : null;
|
|
515
514
|
const { tempDir, workspaceTmpDir, needsClone } = await setupTempDirectory(argv, workspaceInfo);
|
|
@@ -961,10 +960,12 @@ try {
|
|
|
961
960
|
if (logUploadSuccess) {
|
|
962
961
|
await log(' ✅ Logs uploaded successfully');
|
|
963
962
|
} else {
|
|
964
|
-
|
|
963
|
+
// Issue #1212: Always show log upload failures (not just verbose)
|
|
964
|
+
await log(' ⚠️ Failed to upload logs');
|
|
965
965
|
}
|
|
966
966
|
} catch (uploadError) {
|
|
967
|
-
|
|
967
|
+
// Issue #1212: Always show log upload errors (not just verbose)
|
|
968
|
+
await log(` ⚠️ Error uploading logs: ${uploadError.message}`);
|
|
968
969
|
}
|
|
969
970
|
} else if (prNumber) {
|
|
970
971
|
// Fallback: Post simple failure comment if logs are not attached
|
|
@@ -1029,10 +1030,12 @@ try {
|
|
|
1029
1030
|
if (logUploadSuccess) {
|
|
1030
1031
|
await log(' ✅ Logs uploaded successfully');
|
|
1031
1032
|
} else {
|
|
1032
|
-
|
|
1033
|
+
// Issue #1212: Always show log upload failures (not just verbose)
|
|
1034
|
+
await log(' ⚠️ Failed to upload logs');
|
|
1033
1035
|
}
|
|
1034
1036
|
} catch (uploadError) {
|
|
1035
|
-
|
|
1037
|
+
// Issue #1212: Always show log upload errors (not just verbose)
|
|
1038
|
+
await log(` ⚠️ Error uploading logs: ${uploadError.message}`);
|
|
1036
1039
|
}
|
|
1037
1040
|
} else {
|
|
1038
1041
|
// Fallback: Post simple waiting comment if logs are not attached
|
|
@@ -1093,7 +1096,7 @@ try {
|
|
|
1093
1096
|
|
|
1094
1097
|
// If --attach-logs is enabled, attach failure logs before exiting
|
|
1095
1098
|
// Note: sessionId is not required - logs should be uploaded even if agent failed before establishing a session
|
|
1096
|
-
//
|
|
1099
|
+
// Issues #1212, #1462: Fall back to uploading logs to the issue if PR is not available
|
|
1097
1100
|
const hasPR = global.createdPR && global.createdPR.number;
|
|
1098
1101
|
const hasIssue = global.issueNumber;
|
|
1099
1102
|
const logTargetType = hasPR ? 'pr' : hasIssue ? 'issue' : null;
|
|
@@ -1133,10 +1136,12 @@ try {
|
|
|
1133
1136
|
if (logUploadSuccess) {
|
|
1134
1137
|
await log(` ✅ Failure logs uploaded to ${logTargetLabel} successfully`);
|
|
1135
1138
|
} else {
|
|
1136
|
-
|
|
1139
|
+
// Issue #1212: Always show log upload failures (not just verbose)
|
|
1140
|
+
await log(' ⚠️ Failed to upload failure logs');
|
|
1137
1141
|
}
|
|
1138
1142
|
} catch (uploadError) {
|
|
1139
|
-
|
|
1143
|
+
// Issue #1212: Always show log upload errors (not just verbose)
|
|
1144
|
+
await log(` ⚠️ Error uploading failure logs: ${uploadError.message}`);
|
|
1140
1145
|
}
|
|
1141
1146
|
}
|
|
1142
1147
|
|
|
@@ -901,6 +901,11 @@ Thank you!`;
|
|
|
901
901
|
export const classifyCloneError = errorOutput => {
|
|
902
902
|
const output = errorOutput.toLowerCase();
|
|
903
903
|
|
|
904
|
+
// Issue #1211: ENOSPC (disk full) errors - NOT retryable, requires user action
|
|
905
|
+
if (lib.isENOSPC(errorOutput) || output.includes('no space left on device') || (output.includes('unable to write file') && output.includes('error')) || output.includes('errno -28')) {
|
|
906
|
+
return { type: 'ENOSPC', retryable: false, description: 'No space left on device' };
|
|
907
|
+
}
|
|
908
|
+
|
|
904
909
|
// Transient server errors (5xx) - typically retryable
|
|
905
910
|
if (output.includes('error: 500') || output.includes('internal server error') || output.includes('error: 502') || output.includes('error: 503') || output.includes('error: 504')) {
|
|
906
911
|
return { type: 'TRANSIENT', retryable: true, description: 'GitHub server error' };
|
|
@@ -983,36 +988,34 @@ export const cloneRepository = async (repoToClone, tempDir, argv, owner, repo) =
|
|
|
983
988
|
if (line.trim()) await log(` ${line}`);
|
|
984
989
|
}
|
|
985
990
|
await log('');
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
await log('
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
await log('
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
await log('
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
await log(
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
await log(
|
|
1009
|
-
await log(
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
await log('
|
|
1013
|
-
await log(' 6. Use --token flag with different token if available');
|
|
991
|
+
|
|
992
|
+
// Issue #1211: ENOSPC-specific guidance
|
|
993
|
+
if (errorClassification.type === 'ENOSPC') {
|
|
994
|
+
await log(' 💡 Cause: Disk is full — not enough space to clone the repository');
|
|
995
|
+
await log('');
|
|
996
|
+
await log(' 🔧 How to fix:');
|
|
997
|
+
await log(' 1. Free disk space: sudo rm -rf /tmp/* /var/tmp/*');
|
|
998
|
+
await log(' 2. Check disk usage: df -h');
|
|
999
|
+
await log(' 3. Clean Docker/npm: docker system prune -af && npm cache clean --force');
|
|
1000
|
+
await log('');
|
|
1001
|
+
} else {
|
|
1002
|
+
await log(' 💡 Common causes:');
|
|
1003
|
+
await log(" • Repository doesn't exist or is private");
|
|
1004
|
+
await log(' • No GitHub authentication');
|
|
1005
|
+
await log(' • Network connectivity issues');
|
|
1006
|
+
if (errorClassification.type === 'TRANSIENT') await log(' • GitHub server issues (temporary)');
|
|
1007
|
+
if (errorClassification.type === 'RATE_LIMIT') await log(' • API rate limiting exceeded');
|
|
1008
|
+
if (argv.fork) await log(' • Fork not ready yet (try again in a moment)');
|
|
1009
|
+
await log('');
|
|
1010
|
+
await log(' 🔧 How to fix:');
|
|
1011
|
+
await log(' 1. Check authentication: gh auth status');
|
|
1012
|
+
await log(' 2. Login if needed: gh auth login');
|
|
1013
|
+
await log(` 3. Verify access: gh repo view ${owner}/${repo}`);
|
|
1014
|
+
if (argv.fork) await log(` 4. Check fork: gh repo view ${repoToClone}`);
|
|
1015
|
+
if (errorClassification.type === 'TRANSIENT') await log(' 5. Wait and retry / check: https://www.githubstatus.com');
|
|
1016
|
+
if (errorClassification.type === 'RATE_LIMIT') await log(' 5. Wait for rate limit to reset or use --token with different token');
|
|
1017
|
+
await log('');
|
|
1014
1018
|
}
|
|
1015
|
-
await log('');
|
|
1016
1019
|
await safeExit(1, 'Repository setup failed');
|
|
1017
1020
|
}
|
|
1018
1021
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
// Import shared utility from lib.mjs
|
|
18
|
-
import { maskToken, log } from './lib.mjs';
|
|
18
|
+
import { maskToken, log, isENOSPC } from './lib.mjs';
|
|
19
19
|
import { reportError } from './sentry.lib.mjs';
|
|
20
20
|
|
|
21
21
|
// Dynamic imports for runtime dependencies
|
|
@@ -537,11 +537,18 @@ export const sanitizeLogContent = async (logContent, options = {}) => {
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
} catch (error) {
|
|
540
|
+
// Issue #1212: Detect ENOSPC specifically and log at non-verbose level
|
|
541
|
+
const isNoSpace = isENOSPC(error);
|
|
540
542
|
reportError(error, {
|
|
541
543
|
context: 'sanitize_log_content',
|
|
542
|
-
level: 'warning',
|
|
544
|
+
level: isNoSpace ? 'error' : 'warning',
|
|
543
545
|
});
|
|
544
|
-
|
|
546
|
+
if (isNoSpace) {
|
|
547
|
+
await log(` ❌ ENOSPC: No space left on device during log sanitization. Skipping sanitization.`);
|
|
548
|
+
await log(` Consider freeing disk space (e.g., rm -rf ~/.claude/debug/*.txt) and retrying.`);
|
|
549
|
+
} else {
|
|
550
|
+
await log(` ⚠️ Warning: Could not fully sanitize log content: ${error.message}`, { verbose: true });
|
|
551
|
+
}
|
|
545
552
|
}
|
|
546
553
|
|
|
547
554
|
return sanitized;
|