@link-assistant/hive-mind 2.12.5 → 2.13.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.budget-stats.lib.mjs +4 -2
- package/src/claude.lib.mjs +83 -17
- package/src/claude.stream-events.lib.mjs +56 -2
- package/src/disk-guard.lib.mjs +256 -0
- package/src/github.batch.lib.mjs +31 -11
- package/src/github.lib.mjs +3 -1
- package/src/hive.mjs +136 -11
- package/src/lib.mjs +23 -3
- package/src/limits-i18n.lib.mjs +8 -0
- package/src/list-solution-drafts.lib.mjs +17 -4
- package/src/locales/en.lino +10 -0
- package/src/locales/hi.lino +10 -0
- package/src/locales/ru.lino +10 -0
- package/src/locales/zh.lino +10 -0
- package/src/session-log-rename.lib.mjs +65 -0
- package/src/session-monitor.lib.mjs +45 -1
- package/src/solve.mjs +48 -4
- package/src/solve.repository.lib.mjs +5 -1
- package/src/solve.restart-shared.lib.mjs +18 -9
- package/src/solve.results.lib.mjs +6 -3
- package/src/solve.validation.lib.mjs +7 -9
- package/src/subscription-block-telegram.lib.mjs +115 -0
- package/src/subscription-error.lib.mjs +328 -0
- package/src/tool-retry.lib.mjs +29 -0
|
@@ -389,6 +389,35 @@ export async function buildDiskDiagnosticsExtraSection(logPath, { verbose = fals
|
|
|
389
389
|
return '';
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* Issue #2161: Replay the `🚫 SUBSCRIPTION/ACCESS BLOCKED` report from the
|
|
394
|
+
* captured solve log into the Telegram completion message, so the operator is
|
|
395
|
+
* told that the account itself lost access (and not that "claude failed") on the
|
|
396
|
+
* surface they actually watch. Returns '' when the session hit no such block.
|
|
397
|
+
*/
|
|
398
|
+
export async function buildSubscriptionBlockedExtraSection(logPath, { verbose = false, readFile = fs.readFile, locale = null } = {}) {
|
|
399
|
+
if (!logPath) return '';
|
|
400
|
+
try {
|
|
401
|
+
let logText = '';
|
|
402
|
+
try {
|
|
403
|
+
logText = await readFile(logPath, 'utf8');
|
|
404
|
+
} catch (readError) {
|
|
405
|
+
if (verbose) {
|
|
406
|
+
console.log(`[VERBOSE] Could not read session log ${logPath} for subscription block: ${readError?.message || readError}`);
|
|
407
|
+
}
|
|
408
|
+
return '';
|
|
409
|
+
}
|
|
410
|
+
const telegramLib = await import('./subscription-block-telegram.lib.mjs');
|
|
411
|
+
const parsed = telegramLib.parseSubscriptionBlockFromLog(logText);
|
|
412
|
+
if (!parsed) return '';
|
|
413
|
+
return telegramLib.formatSubscriptionBlockedSection(parsed, { locale });
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (verbose) {
|
|
416
|
+
console.log(`[VERBOSE] Could not build subscription block section for ${logPath}: ${error?.message || error}`);
|
|
417
|
+
}
|
|
418
|
+
return '';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
392
421
|
async function getDockerContainerFilesystemSizeForSession(sessionName, sessionInfo, { verbose = false, sizeProvider = null } = {}) {
|
|
393
422
|
if (sessionInfo?.isolationBackend !== 'docker') return null;
|
|
394
423
|
const containerName = sessionInfo.sessionId || sessionName;
|
|
@@ -826,6 +855,21 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
826
855
|
}
|
|
827
856
|
}
|
|
828
857
|
const dockerTaskContainerExtraSections = dockerTaskContainerAction?.extraSection ? [dockerTaskContainerAction.extraSection] : [];
|
|
858
|
+
// Issue #2161: a blocked subscription/account explains every other
|
|
859
|
+
// symptom of the run, so it goes first in the completion message.
|
|
860
|
+
const subscriptionBlockedExtraSections = [];
|
|
861
|
+
try {
|
|
862
|
+
const blockedSection = await buildSubscriptionBlockedExtraSection(statusResult?.logPath || sessionInfo?.logPath || null, {
|
|
863
|
+
verbose,
|
|
864
|
+
readFile: options.readFile,
|
|
865
|
+
locale: sessionInfo?.locale || null,
|
|
866
|
+
});
|
|
867
|
+
if (blockedSection) subscriptionBlockedExtraSections.push(blockedSection);
|
|
868
|
+
} catch (blockedError) {
|
|
869
|
+
if (verbose) {
|
|
870
|
+
console.log(`[VERBOSE] Could not build subscription block section for ${sessionName}: ${blockedError?.message || blockedError}`);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
829
873
|
// Issue #2134: say exactly WHY a session was killed, and warn when a
|
|
830
874
|
// session merely survived a kill event instead of reporting a plain
|
|
831
875
|
// success. The pull request gets the very same report below.
|
|
@@ -869,7 +913,7 @@ export async function monitorSessions(bot, verbose = false, options = {}) {
|
|
|
869
913
|
infoBlock: sessionInfo?.infoBlock || '',
|
|
870
914
|
pullRequestUrl,
|
|
871
915
|
pullRequestState,
|
|
872
|
-
extraSections: [...limitsExtraSections, ...killReport.sections, ...resumeExtraSections, ...diskExtraSections, ...dockerTaskContainerExtraSections],
|
|
916
|
+
extraSections: [...subscriptionBlockedExtraSections, ...limitsExtraSections, ...killReport.sections, ...resumeExtraSections, ...diskExtraSections, ...dockerTaskContainerExtraSections],
|
|
873
917
|
});
|
|
874
918
|
if (killReport.killed || killReport.recovered) {
|
|
875
919
|
const notice = await announceKillOnPullRequest({
|
package/src/solve.mjs
CHANGED
|
@@ -19,7 +19,9 @@ const fs = (await use('fs')).promises;
|
|
|
19
19
|
const crypto = (await use('crypto')).default;
|
|
20
20
|
const memoryCheck = await import('./memory-check.mjs');
|
|
21
21
|
const lib = await import('./lib.mjs');
|
|
22
|
-
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, formatToolExecutionFailure, getVersionInfo, logSolveStartup, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
|
|
22
|
+
const { log, setLogFile, getLogFile, getAbsoluteLogPath, cleanErrorMessage, formatAligned, formatToolExecutionFailure, extractToolErrorCore, getVersionInfo, logSolveStartup, setupVerboseLogInterceptor, setupStdioLogInterceptor } = lib;
|
|
23
|
+
// Issue #2161: terminal subscription/account-access blocks.
|
|
24
|
+
const { detectSubscriptionError, formatSubscriptionErrorReport, formatSubscriptionErrorSummary, SUBSCRIPTION_BLOCKED_MARKER } = await import('./subscription-error.lib.mjs');
|
|
23
25
|
const githubLib = await import('./github.lib.mjs');
|
|
24
26
|
const { sanitizeLogContent, attachLogToGitHub, getToolDisplayName } = githubLib;
|
|
25
27
|
const validation = await import('./solve.validation.lib.mjs');
|
|
@@ -221,6 +223,15 @@ const skipToolConnectionCheck = prepareOnly || argv.skipToolConnectionCheck || a
|
|
|
221
223
|
const { cascadePlaywrightMcpDisable, ensureSolvePlaywrightMcpReady } = await import('./playwright-mcp.lib.mjs');
|
|
222
224
|
await cascadePlaywrightMcpDisable(argv, log);
|
|
223
225
|
if (!(await performSystemChecks(argv.minDiskSpace || 10240, skipToolConnectionCheck, argv.model, argv))) {
|
|
226
|
+
// Issue #2160: an exhausted host disk is an environment condition, not a defect in the issue.
|
|
227
|
+
// Exit with EX_TEMPFAIL (75) so an orchestrator can requeue the task, and skip the pre-exit
|
|
228
|
+
// notifier: posting "🚨 Solution Draft Failed — Reason: System checks failed" on the target
|
|
229
|
+
// repository's issue told its maintainers nothing they could act on.
|
|
230
|
+
if (argv.systemCheckFailure?.check === 'disk-space') {
|
|
231
|
+
const { EXIT_CODE_INSUFFICIENT_DISK_SPACE } = await import('./disk-guard.lib.mjs');
|
|
232
|
+
const { availableMB, requiredMB } = argv.systemCheckFailure;
|
|
233
|
+
await safeExit(EXIT_CODE_INSUFFICIENT_DISK_SPACE, `Insufficient disk space on this host (${availableMB}MB available, ${requiredMB}MB required) — the issue itself was not attempted`, { skipPreExit: true });
|
|
234
|
+
}
|
|
224
235
|
await safeExit(1, 'System checks failed');
|
|
225
236
|
}
|
|
226
237
|
// Playwright MCP preflight is local/free and stays independent from paid tool connection checks.
|
|
@@ -269,6 +280,9 @@ const { isPublic: isRepoPublic } = await detectRepositoryVisibility(owner, repo)
|
|
|
269
280
|
if (argv.autoCleanup === undefined) {
|
|
270
281
|
// For public repos: keep temp directories (default false) For private repos: clean up temp directories (default true)
|
|
271
282
|
argv.autoCleanup = !isRepoPublic;
|
|
283
|
+
// Issue #2160: remember that this was a default, not a flag, so the "keeping directory"
|
|
284
|
+
// message at the end of the session can say why the workspace is being kept.
|
|
285
|
+
argv.autoCleanupSource = 'repository-visibility-default';
|
|
272
286
|
if (argv.verbose) {
|
|
273
287
|
await log(` Auto-cleanup default: ${argv.autoCleanup} (repository is ${isRepoPublic ? 'public' : 'private'})`, {
|
|
274
288
|
verbose: true,
|
|
@@ -1000,6 +1014,15 @@ try {
|
|
|
1000
1014
|
const toolForFailure = argv.tool || 'claude';
|
|
1001
1015
|
// Issue #1845: surface the core error instead of just "<TOOL> execution failed" (terminal + comment).
|
|
1002
1016
|
const toolFailureMessage = formatToolExecutionFailure({ tool: toolForFailure, toolResult });
|
|
1017
|
+
// Issue #2161: an account/subscription block ("Your organization has disabled
|
|
1018
|
+
// Claude subscription access for Claude Code", a revoked OAuth token, an
|
|
1019
|
+
// expired plan) is terminal — the run must stop, say precisely what happened
|
|
1020
|
+
// and preserve the work, instead of ending on a bare "<TOOL> execution failed
|
|
1021
|
+
// with <provider sentence>". Adapters that parse structured provider codes
|
|
1022
|
+
// (claude.lib.mjs) hand the classification over directly; for every other tool
|
|
1023
|
+
// the rendered message is re-classified here, so the whole failure surface is
|
|
1024
|
+
// covered by one chokepoint.
|
|
1025
|
+
const subscriptionInfo = toolResult?.subscriptionError || detectSubscriptionError({ message: extractToolErrorCore({ toolResult }) || toolFailureMessage, tool: toolForFailure });
|
|
1003
1026
|
if (sessionId) {
|
|
1004
1027
|
await log('');
|
|
1005
1028
|
await log('💡 To continue this session:');
|
|
@@ -1014,15 +1037,32 @@ try {
|
|
|
1014
1037
|
await log('');
|
|
1015
1038
|
}
|
|
1016
1039
|
// Preserve work before remote diagnostics; issue #2101 ended during log upload.
|
|
1040
|
+
let preservedWork = null;
|
|
1017
1041
|
try {
|
|
1018
1042
|
const { criticalErrorRecovery } = await import('./config.lib.mjs');
|
|
1019
1043
|
if (criticalErrorRecovery.autoCommitUncommittedChanges) {
|
|
1020
1044
|
const { commitUncommittedChangesOnCriticalError } = await import('./critical-error-commit.lib.mjs');
|
|
1021
|
-
|
|
1045
|
+
// Issue #2161: when the subscription is gone it is unknown whether/when it
|
|
1046
|
+
// will be restored, so the emergency commit is the only thing standing
|
|
1047
|
+
// between the operator and hours of lost work — name it as such.
|
|
1048
|
+
preservedWork = await commitUncommittedChangesOnCriticalError({ tempDir, branchName, $, log, reason: subscriptionInfo ? formatSubscriptionErrorSummary(subscriptionInfo, { tool: toolForFailure }) : toolFailureMessage });
|
|
1022
1049
|
}
|
|
1023
1050
|
} catch (preserveError) {
|
|
1024
1051
|
await log(` ⚠️ Could not auto-commit before failure exit: ${preserveError.message}`, { verbose: true });
|
|
1025
1052
|
}
|
|
1053
|
+
// Issue #2161: printed after the emergency commit so the block can state
|
|
1054
|
+
// whether the work was preserved. This is the message the operator reads.
|
|
1055
|
+
if (subscriptionInfo) {
|
|
1056
|
+
const reportLines = formatSubscriptionErrorReport(subscriptionInfo, {
|
|
1057
|
+
tool: toolForFailure,
|
|
1058
|
+
sessionId,
|
|
1059
|
+
tempDir,
|
|
1060
|
+
branchName,
|
|
1061
|
+
committed: preservedWork ? preservedWork.committed : null,
|
|
1062
|
+
resumeCommand: sessionId && argv.url ? buildSolveResumeCommand({ issueUrl: argv.url, sessionId, tool: toolForFailure, model: argv.model, fallbackModel: argv.fallbackModel, tempDir }) : null,
|
|
1063
|
+
});
|
|
1064
|
+
for (const line of reportLines) await log(line, { level: 'error' });
|
|
1065
|
+
}
|
|
1026
1066
|
// Attach failure logs before exiting (Issues #1212, #1462: fall back to issue if no PR)
|
|
1027
1067
|
const hasPR = global.createdPR && global.createdPR.number;
|
|
1028
1068
|
const hasIssue = global.issueNumber;
|
|
@@ -1052,7 +1092,9 @@ try {
|
|
|
1052
1092
|
// Include sessionId so the PR comment can present it
|
|
1053
1093
|
sessionId,
|
|
1054
1094
|
// If not a usage limit case, fall back to generic failure format
|
|
1055
|
-
|
|
1095
|
+
// Issue #2161: the PR/issue comment gets the diagnosis + the remediation
|
|
1096
|
+
// steps too — whoever finds the run in the morning reads that, not the log.
|
|
1097
|
+
errorMessage: limitReached ? undefined : subscriptionInfo ? [formatSubscriptionErrorSummary(subscriptionInfo, { tool: toolForFailure }), '', ...(subscriptionInfo.guidance || []).map(step => `- ${step}`)].join('\n') : toolFailureMessage,
|
|
1056
1098
|
argv,
|
|
1057
1099
|
requestedModel: argv.originalModel || argv.model,
|
|
1058
1100
|
tool: argv.tool || 'claude',
|
|
@@ -1070,7 +1112,9 @@ try {
|
|
|
1070
1112
|
await log(` ⚠️ Error uploading failure logs: ${uploadError.message}`);
|
|
1071
1113
|
}
|
|
1072
1114
|
}
|
|
1073
|
-
|
|
1115
|
+
// Issue #2161: the exit message is what /hive and the session monitor see, so
|
|
1116
|
+
// it carries the marker rather than the generic tool-failure sentence.
|
|
1117
|
+
await safeExit(1, subscriptionInfo ? `${SUBSCRIPTION_BLOCKED_MARKER} — ${formatSubscriptionErrorSummary(subscriptionInfo, { tool: toolForFailure })}` : toolFailureMessage);
|
|
1074
1118
|
}
|
|
1075
1119
|
// Clean up .playwright-mcp/ to prevent browser artifacts from triggering auto-restart (Issue #1124)
|
|
1076
1120
|
if (argv.playwrightMcpAutoCleanup !== false) {
|
|
@@ -1338,6 +1338,10 @@ export const cleanupTempDirectory = async (tempDir, argv, limitReached) => {
|
|
|
1338
1338
|
} else if (limitReached) {
|
|
1339
1339
|
await log(`\n📁 Keeping directory for future resume: ${tempDir}`);
|
|
1340
1340
|
} else if (!argv.autoCleanup) {
|
|
1341
|
-
|
|
1341
|
+
// Issue #2160: `--no-auto-cleanup` is only one of the two ways to get here. On a public
|
|
1342
|
+
// repository auto-cleanup defaults to off, and reporting a flag that was never passed made
|
|
1343
|
+
// the run log misleading — the disk kept filling with no hint of why.
|
|
1344
|
+
const reason = argv.autoCleanupSource === 'repository-visibility-default' ? 'auto-cleanup is off by default for public repositories' : '--no-auto-cleanup';
|
|
1345
|
+
await log(`\n📁 Keeping directory (${reason}): ${tempDir}`);
|
|
1342
1346
|
}
|
|
1343
1347
|
};
|
|
@@ -30,7 +30,10 @@ const fs = (await use('fs')).promises;
|
|
|
30
30
|
|
|
31
31
|
// Import shared library functions
|
|
32
32
|
const lib = await import('./lib.mjs');
|
|
33
|
-
|
|
33
|
+
// Issue #2160: the real log-file accessors must be forwarded to every tool executor. Passing
|
|
34
|
+
// no-op stubs (or omitting them entirely) broke session-log renaming in restart/watch iterations
|
|
35
|
+
// ("⚠️ Could not rename log file: getLogFile is not a function").
|
|
36
|
+
const { log, formatAligned, extractToolErrorCore, getLogFile, setLogFile } = lib;
|
|
34
37
|
const { ensurePullRequestBaseBranch } = await import('./solve.pr-base-guard.lib.mjs');
|
|
35
38
|
const { ensureAiToolScratchIgnored, filterAiToolScratchFromStatus } = await import('./ai-tool-scratch.lib.mjs');
|
|
36
39
|
const { RESOURCE_PHASE_RESTART_AFTER, RESOURCE_PHASE_RESTART_BEFORE, recordResourceSnapshot } = await import('./solve.resource-diagnostics.lib.mjs');
|
|
@@ -240,8 +243,8 @@ export const executeToolIteration = async params => {
|
|
|
240
243
|
log,
|
|
241
244
|
formatAligned,
|
|
242
245
|
getResourceSnapshot,
|
|
243
|
-
setLogFile
|
|
244
|
-
getLogFile
|
|
246
|
+
setLogFile,
|
|
247
|
+
getLogFile,
|
|
245
248
|
$,
|
|
246
249
|
});
|
|
247
250
|
} else if (argv.tool === 'opencode') {
|
|
@@ -280,6 +283,8 @@ export const executeToolIteration = async params => {
|
|
|
280
283
|
log,
|
|
281
284
|
formatAligned,
|
|
282
285
|
getResourceSnapshot,
|
|
286
|
+
setLogFile,
|
|
287
|
+
getLogFile,
|
|
283
288
|
opencodePath,
|
|
284
289
|
$,
|
|
285
290
|
});
|
|
@@ -318,8 +323,8 @@ export const executeToolIteration = async params => {
|
|
|
318
323
|
repo,
|
|
319
324
|
argv,
|
|
320
325
|
log,
|
|
321
|
-
setLogFile
|
|
322
|
-
getLogFile
|
|
326
|
+
setLogFile,
|
|
327
|
+
getLogFile,
|
|
323
328
|
formatAligned,
|
|
324
329
|
getResourceSnapshot,
|
|
325
330
|
codexPath,
|
|
@@ -362,6 +367,8 @@ export const executeToolIteration = async params => {
|
|
|
362
367
|
log,
|
|
363
368
|
formatAligned,
|
|
364
369
|
getResourceSnapshot,
|
|
370
|
+
setLogFile,
|
|
371
|
+
getLogFile,
|
|
365
372
|
agentPath,
|
|
366
373
|
$,
|
|
367
374
|
});
|
|
@@ -400,8 +407,8 @@ export const executeToolIteration = async params => {
|
|
|
400
407
|
repo,
|
|
401
408
|
argv,
|
|
402
409
|
log,
|
|
403
|
-
setLogFile
|
|
404
|
-
getLogFile
|
|
410
|
+
setLogFile,
|
|
411
|
+
getLogFile,
|
|
405
412
|
formatAligned,
|
|
406
413
|
getResourceSnapshot,
|
|
407
414
|
geminiPath,
|
|
@@ -442,8 +449,8 @@ export const executeToolIteration = async params => {
|
|
|
442
449
|
repo,
|
|
443
450
|
argv,
|
|
444
451
|
log,
|
|
445
|
-
setLogFile
|
|
446
|
-
getLogFile
|
|
452
|
+
setLogFile,
|
|
453
|
+
getLogFile,
|
|
447
454
|
formatAligned,
|
|
448
455
|
getResourceSnapshot,
|
|
449
456
|
qwenPath,
|
|
@@ -488,6 +495,8 @@ export const executeToolIteration = async params => {
|
|
|
488
495
|
log,
|
|
489
496
|
formatAligned,
|
|
490
497
|
getResourceSnapshot,
|
|
498
|
+
setLogFile,
|
|
499
|
+
getLogFile,
|
|
491
500
|
claudePath,
|
|
492
501
|
$,
|
|
493
502
|
});
|
|
@@ -485,12 +485,15 @@ export const cleanupClaudeFile = async (tempDir, branchName, claudeCommitHash =
|
|
|
485
485
|
const verifyResult = await $({ cwd: tempDir })`git ls-files ${fileName} 2>&1`;
|
|
486
486
|
const fileStillExists = verifyResult.code === 0 && verifyResult.stdout && verifyResult.stdout.trim();
|
|
487
487
|
if (fileStillExists) {
|
|
488
|
-
|
|
489
|
-
//
|
|
488
|
+
// Issue #2160: the pre-existence check must come FIRST. A file that legitimately predates
|
|
489
|
+
// the session is not a cleanup failure, and warning about it produced a false positive
|
|
490
|
+
// ("⚠️ WARNING: .gitkeep still exists after cleanup" immediately followed by
|
|
491
|
+
// "ℹ️ .gitkeep existed before this session — keeping pre-existing file").
|
|
490
492
|
const parentCommit = `${claudeCommitHash}~1`;
|
|
491
493
|
const parentFileExists = await $({ cwd: tempDir })`git cat-file -e ${parentCommit}:${fileName} 2>&1`;
|
|
492
494
|
if (parentFileExists.code !== 0) {
|
|
493
|
-
// File didn't exist before the session — force remove it
|
|
495
|
+
// File didn't exist before the session — this is a real leftover, force remove it
|
|
496
|
+
await log(` ⚠️ WARNING: ${fileName} still exists after cleanup — attempting direct removal...`);
|
|
494
497
|
await $({ cwd: tempDir })`git rm -f ${fileName} 2>&1`;
|
|
495
498
|
const fallbackCommit = await $({ cwd: tempDir })`git commit -m "Remove leftover ${fileName} (post-cleanup fallback, Issue #1436)" 2>&1`;
|
|
496
499
|
if (fallbackCommit.code === 0) {
|
|
@@ -53,12 +53,6 @@ const { parseResetTime: parseResetTimeToDate } = usageLimitLib;
|
|
|
53
53
|
|
|
54
54
|
const { validateClaudeConnection } = claudeLib;
|
|
55
55
|
|
|
56
|
-
// Wrapper function for disk space check using imported module
|
|
57
|
-
const checkDiskSpace = async (minSpaceMB = 10240) => {
|
|
58
|
-
const result = await memoryCheck.checkDiskSpace(minSpaceMB, { log });
|
|
59
|
-
return result.success;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
56
|
// Wrapper function for memory check using imported module
|
|
63
57
|
const checkMemory = async (minMemoryMB = 256) => {
|
|
64
58
|
const result = await memoryCheck.checkMemory(minMemoryMB, { log });
|
|
@@ -217,9 +211,13 @@ export const validateContinueOnlyOnFeedback = async (argv, isPrUrl, isIssueUrl)
|
|
|
217
211
|
// Note: skipToolConnection only skips the connection check, not model validation
|
|
218
212
|
// Model validation should be done separately before calling this function
|
|
219
213
|
export const performSystemChecks = async (minDiskSpace = 10240, skipToolConnection = false, model = 'sonnet', argv = {}) => {
|
|
220
|
-
// Check disk space before proceeding
|
|
221
|
-
|
|
222
|
-
|
|
214
|
+
// Check disk space before proceeding.
|
|
215
|
+
// Issue #2160: record *which* check failed on argv. A full disk says nothing about the issue
|
|
216
|
+
// being solved, so the caller exits with a retry-later code and skips the "Solution Draft
|
|
217
|
+
// Failed" comment instead of blaming the task (hive counted 4 such exits as task failures).
|
|
218
|
+
const diskSpace = await memoryCheck.checkDiskSpace(minDiskSpace, { log });
|
|
219
|
+
if (!diskSpace.success) {
|
|
220
|
+
argv.systemCheckFailure = { check: 'disk-space', availableMB: diskSpace.availableMB, requiredMB: minDiskSpace };
|
|
223
221
|
return false;
|
|
224
222
|
}
|
|
225
223
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// Issue #2161: Telegram surface for subscription/account-access blocks.
|
|
2
|
+
//
|
|
3
|
+
// `/solve` prints a SUBSCRIPTION_BLOCKED_MARKER report into the session log when
|
|
4
|
+
// the account can no longer use the agent tool (expired/cancelled Claude MAX
|
|
5
|
+
// subscription, org policy, revoked ChatGPT/Codex entitlement, ...). The session
|
|
6
|
+
// monitor captures that log, so the same block can be replayed into the Telegram
|
|
7
|
+
// completion message without any extra plumbing between processes.
|
|
8
|
+
|
|
9
|
+
import { SUBSCRIPTION_BLOCKED_MARKER } from './subscription-error.lib.mjs';
|
|
10
|
+
import { lt } from './limits-i18n.lib.mjs';
|
|
11
|
+
|
|
12
|
+
const MAX_MESSAGE_LENGTH = 400;
|
|
13
|
+
const MAX_GUIDANCE_STEPS = 4;
|
|
14
|
+
|
|
15
|
+
const truncate = (value, limit = MAX_MESSAGE_LENGTH) => {
|
|
16
|
+
const text = String(value || '').trim();
|
|
17
|
+
if (text.length <= limit) return text;
|
|
18
|
+
return `${text.slice(0, limit - 1)}…`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const stripPrefix = (line, prefix) => line.slice(line.indexOf(prefix) + prefix.length).trim();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse the last SUBSCRIPTION_BLOCKED_MARKER report out of a captured session log.
|
|
25
|
+
*
|
|
26
|
+
* The report is emitted by formatSubscriptionErrorReport(); every line after the
|
|
27
|
+
* marker line is indented, so the block ends at the first non-indented line.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} logText
|
|
30
|
+
* @returns {null|{tool: string|null, label: string|null, message: string|null, code: string|null, reason: string|null, guidance: string[], committed: boolean|null, resumeCommand: string|null}}
|
|
31
|
+
*/
|
|
32
|
+
export function parseSubscriptionBlockFromLog(logText) {
|
|
33
|
+
if (!logText || typeof logText !== 'string') return null;
|
|
34
|
+
if (!logText.includes(SUBSCRIPTION_BLOCKED_MARKER)) return null;
|
|
35
|
+
|
|
36
|
+
const lines = logText.split('\n');
|
|
37
|
+
// Walk backwards: the richest report (from /solve) is the last one printed.
|
|
38
|
+
let markerIndex = -1;
|
|
39
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
40
|
+
if (lines[i].includes(SUBSCRIPTION_BLOCKED_MARKER)) {
|
|
41
|
+
markerIndex = i;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (markerIndex === -1) return null;
|
|
46
|
+
|
|
47
|
+
const headline = stripPrefix(lines[markerIndex], SUBSCRIPTION_BLOCKED_MARKER).replace(/^—\s*/, '');
|
|
48
|
+
const separator = headline.indexOf(':');
|
|
49
|
+
const parsed = {
|
|
50
|
+
tool: separator > 0 ? headline.slice(0, separator).trim() : null,
|
|
51
|
+
label: separator > 0 ? headline.slice(separator + 1).trim() : headline || null,
|
|
52
|
+
message: null,
|
|
53
|
+
code: null,
|
|
54
|
+
reason: null,
|
|
55
|
+
guidance: [],
|
|
56
|
+
committed: null,
|
|
57
|
+
resumeCommand: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (let i = markerIndex + 1; i < lines.length; i += 1) {
|
|
61
|
+
const raw = lines[i];
|
|
62
|
+
if (!raw.trim()) continue;
|
|
63
|
+
if (!/^\s{3}/.test(raw)) break; // end of the indented report block
|
|
64
|
+
const line = raw.trim();
|
|
65
|
+
if (line.startsWith('Provider said:')) parsed.message = stripPrefix(line, 'Provider said:');
|
|
66
|
+
else if (line.startsWith('Error code:')) parsed.code = stripPrefix(line, 'Error code:');
|
|
67
|
+
else if (line.startsWith('HTTP status:')) parsed.code = `HTTP ${stripPrefix(line, 'HTTP status:')}`;
|
|
68
|
+
else if (line.startsWith('Why this stops the run:')) parsed.reason = stripPrefix(line, 'Why this stops the run:');
|
|
69
|
+
else if (line.startsWith('•')) parsed.guidance.push(line.slice(1).trim());
|
|
70
|
+
else if (line.startsWith('💾')) parsed.committed = true;
|
|
71
|
+
else if (line.startsWith('⚠️')) parsed.committed = false;
|
|
72
|
+
else if (line.startsWith('▶️')) parsed.resumeCommand = stripPrefix(line, ':');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render the parsed block as a Telegram extraSection (title + fenced body), the
|
|
80
|
+
* same shape formatDiskDiagnosticsBlock() uses.
|
|
81
|
+
*
|
|
82
|
+
* @returns {string} empty string when there is nothing to show
|
|
83
|
+
*/
|
|
84
|
+
export function formatSubscriptionBlockedSection(parsed, { locale = null } = {}) {
|
|
85
|
+
if (!parsed) return '';
|
|
86
|
+
const options = locale ? { locale } : {};
|
|
87
|
+
const body = [];
|
|
88
|
+
|
|
89
|
+
const label = parsed.label || lt('subscription_blocked_title', {}, options);
|
|
90
|
+
body.push(parsed.tool ? `${parsed.tool}: ${label}` : label);
|
|
91
|
+
if (parsed.message) body.push(`${lt('subscription_blocked_provider', {}, options)}: ${truncate(parsed.message)}`);
|
|
92
|
+
if (parsed.code) body.push(`${lt('subscription_blocked_code', {}, options)}: ${parsed.code}`);
|
|
93
|
+
if (parsed.reason) body.push(`${lt('subscription_blocked_reason', {}, options)}: ${parsed.reason}`);
|
|
94
|
+
body.push(lt('subscription_blocked_note', {}, options));
|
|
95
|
+
if (parsed.guidance.length) {
|
|
96
|
+
body.push('');
|
|
97
|
+
body.push(`${lt('subscription_blocked_steps', {}, options)}:`);
|
|
98
|
+
for (const step of parsed.guidance.slice(0, MAX_GUIDANCE_STEPS)) body.push(` • ${step}`);
|
|
99
|
+
}
|
|
100
|
+
if (parsed.committed === true) {
|
|
101
|
+
body.push('');
|
|
102
|
+
body.push(lt('subscription_blocked_preserved', {}, options));
|
|
103
|
+
}
|
|
104
|
+
if (parsed.resumeCommand) {
|
|
105
|
+
body.push('');
|
|
106
|
+
body.push(`${lt('subscription_blocked_resume', {}, options)}: ${parsed.resumeCommand}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `🚫 ${lt('subscription_blocked_title', {}, options)}\n\`\`\`\n${body.join('\n')}\n\`\`\``;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default {
|
|
113
|
+
parseSubscriptionBlockFromLog,
|
|
114
|
+
formatSubscriptionBlockedSection,
|
|
115
|
+
};
|