@link-assistant/hive-mind 2.15.2 → 2.16.0
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 +19 -0
- package/package.json +1 -1
- package/src/bot-lifecycle.lib.mjs +8 -1
- package/src/child-exit.lib.mjs +53 -1
- package/src/claude.session-tokens.lib.mjs +10 -8
- package/src/claude.session-transcript-repair.lib.mjs +65 -34
- package/src/codex.lib.mjs +11 -1
- package/src/development-log.lib.mjs +22 -7
- package/src/github-error-reporter.lib.mjs +84 -2
- package/src/github.lib.mjs +212 -152
- package/src/log-bounded-read.lib.mjs +411 -0
- package/src/log-sanitize-stream.lib.mjs +267 -0
- package/src/log-sanitize-worker-entry.mjs +31 -0
- package/src/log-sanitize-worker.lib.mjs +186 -0
- package/src/log-upload.lib.mjs +16 -4
- package/src/session-completion-state.lib.mjs +124 -0
- package/src/session-kill-diagnostics.lib.mjs +73 -10
- package/src/session-kill-policy.lib.mjs +18 -7
- package/src/session-kill-resume.lib.mjs +5 -2
- package/src/session-monitor.lib.mjs +132 -9
- package/src/session-store.lib.mjs +15 -1
- package/src/solve.config.lib.mjs +5 -2
- package/src/solve.resource-diagnostics.lib.mjs +71 -4
- package/src/telegram-log-command.lib.mjs +7 -3
- package/src/telegram-terminal-watch-command.lib.mjs +9 -1
package/src/github.lib.mjs
CHANGED
|
@@ -10,6 +10,10 @@ import { batchCheckPullRequestsForIssues as batchCheckPRs, batchCheckArchivedRep
|
|
|
10
10
|
import { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent, sanitizeForPublication, writeSanitizedPublicationFile } from './token-sanitization.lib.mjs';
|
|
11
11
|
export { isSafeToken, isHexInSafeContext, getGitHubTokensFromFiles, getGitHubTokensFromCommand, sanitizeOutput, sanitizeLogContent, sanitizeForPublication, writeSanitizedPublicationFile }; // Re-export for backward compatibility
|
|
12
12
|
import { uploadLogWithGhUploadLog } from './log-upload.lib.mjs';
|
|
13
|
+
// Issue #2189: bracket the log-upload phase with resource samples. The incident
|
|
14
|
+
// log's last sample was `after_agent`, ten minutes before the heap OOM, so the
|
|
15
|
+
// phase that actually died left no telemetry at all.
|
|
16
|
+
import { recordResourceSnapshot, RESOURCE_PHASE_LOG_UPLOAD_START, RESOURCE_PHASE_LOG_UPLOAD_END } from './solve.resource-diagnostics.lib.mjs';
|
|
13
17
|
import { formatResetTimeWithRelative } from './usage-limit.lib.mjs'; // See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
14
18
|
// Import model info helpers (Issue #1225)
|
|
15
19
|
import { getToolDisplayName, getModelInfoForComment } from './models/index.mjs';
|
|
@@ -26,6 +30,13 @@ export { buildGitHubPullRequestUrl, buildGitHubPullRequestUrlOrNull, isGitHubUrl
|
|
|
26
30
|
import { SOLUTION_DRAFT_LOG_MARKER, SOLUTION_DRAFT_FAILED_MARKER, SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER, USAGE_LIMIT_REACHED_MARKER, NOW_WORKING_SESSION_IS_ENDED_MARKER, postTrackedComment, postTrackedCommentFromFile } from './tool-comments.lib.mjs';
|
|
27
31
|
export const maskGitHubToken = maskToken; // Alias for backward compatibility
|
|
28
32
|
export const escapeCodeBlocksInLog = logContent => logContent.replace(/```/g, '\\`\\`\\`'); // Escape ``` in logs
|
|
33
|
+
// Issue #2189: the old "too large" message always rendered megabytes, so a 200 KB
|
|
34
|
+
// log that still could not be inlined was reported as "0MB". Report the unit that
|
|
35
|
+
// actually carries information.
|
|
36
|
+
export const formatLogSizeForHumans = bytes => {
|
|
37
|
+
const size = Number(bytes) || 0;
|
|
38
|
+
return size >= 1024 * 1024 ? `${Math.round(size / 1024 / 1024)}MB` : `${Math.round(size / 1024)}KB`;
|
|
39
|
+
};
|
|
29
40
|
const buildIssueFailureActionSection = targetType => {
|
|
30
41
|
if (targetType !== 'issue') return '';
|
|
31
42
|
return `
|
|
@@ -317,6 +328,150 @@ const getLogUploadTerminalStatus = ({ errorMessage, errorDuringExecution, isUsag
|
|
|
317
328
|
if (isUsageLimit) return { emoji: '📎', label: 'Usage-limit execution log' };
|
|
318
329
|
return { emoji: '✅', label: 'Solution draft log' };
|
|
319
330
|
};
|
|
331
|
+
/**
|
|
332
|
+
* Build the inline `--attach-logs` comment body — the variant that embeds the
|
|
333
|
+
* whole transcript inside a `<details>` block.
|
|
334
|
+
*
|
|
335
|
+
* Issue #2189: extracted from attachLogToGitHub so that it is only ever called
|
|
336
|
+
* for a log small enough to inline. Previously the body was assembled
|
|
337
|
+
* unconditionally — including reading, sanitizing and escaping the log — and
|
|
338
|
+
* then discarded when the size check downstream routed the log to gh-upload-log
|
|
339
|
+
* anyway. On a 134 MB transcript that discarded pre-work alone exhausted V8's
|
|
340
|
+
* old space and killed the run.
|
|
341
|
+
*
|
|
342
|
+
* @param {object} params - Everything the four comment shapes interpolate
|
|
343
|
+
* @param {string} params.logContent - Already sanitized and markdown-escaped log
|
|
344
|
+
* @param {number} params.logSizeBytes - Size of the log on disk, for the summary line
|
|
345
|
+
* @returns {string} Markdown comment body
|
|
346
|
+
*/
|
|
347
|
+
function buildInlineLogComment({ logContent, logSizeBytes, targetType, customTitle, sessionType, sessionId, errorMessage, errorDuringExecution, isUsageLimit, limitResetTime, toolName, resumeCommand, isAutoResumeEnabled, autoResumeMode, modelInfoString, budgetStats, totalCostUSD, anthropicTotalCostUSD, pricingInfo, failureAction }) {
|
|
348
|
+
let logComment;
|
|
349
|
+
// Usage limit comments should be shown whenever isUsageLimit is true, regardless of whether a generic errorMessage is provided.
|
|
350
|
+
if (isUsageLimit) {
|
|
351
|
+
// Usage limit error format - separate from general failures
|
|
352
|
+
logComment = `## ⏳ ${USAGE_LIMIT_REACHED_MARKER}
|
|
353
|
+
|
|
354
|
+
The automated solution draft was interrupted because the ${toolName} usage limit was reached.
|
|
355
|
+
|
|
356
|
+
### 📊 Limit Information
|
|
357
|
+
- **Tool**: ${toolName}
|
|
358
|
+
- **Limit Type**: Usage limit exceeded`;
|
|
359
|
+
if (limitResetTime) {
|
|
360
|
+
// Format reset time with relative time and UTC for better user understanding Shows "in 14m (Feb 6, 3:00 PM UTC)" instead of just "4:00 PM" See: https://github.com/link-assistant/hive-mind/issues/1236
|
|
361
|
+
const formattedResetTime = formatResetTimeWithRelative(limitResetTime, global.limitTimezone || null) || limitResetTime;
|
|
362
|
+
logComment += `\n- **Reset Time**: ${formattedResetTime}`;
|
|
363
|
+
}
|
|
364
|
+
if (sessionId) {
|
|
365
|
+
logComment += `\n- **Session ID**: ${sessionId}`;
|
|
366
|
+
}
|
|
367
|
+
logComment += '\n\n### 🔄 How to Continue\n';
|
|
368
|
+
// If auto-resume/auto-restart is enabled, show automatic continuation message instead of CLI commands See: https://github.com/link-assistant/hive-mind/issues/1152
|
|
369
|
+
if (isAutoResumeEnabled) {
|
|
370
|
+
const modeName = autoResumeMode === 'restart' ? 'restart' : 'resume';
|
|
371
|
+
const modeDescription = autoResumeMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
|
|
372
|
+
logComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
|
|
373
|
+
} else {
|
|
374
|
+
// Manual resume mode - show CLI commands
|
|
375
|
+
if (limitResetTime) {
|
|
376
|
+
logComment += `Once the limit resets at **${limitResetTime}**, `;
|
|
377
|
+
} else {
|
|
378
|
+
logComment += 'Once the limit resets, ';
|
|
379
|
+
}
|
|
380
|
+
if (resumeCommand) {
|
|
381
|
+
logComment += `you can resume this session by running:
|
|
382
|
+
\`\`\`bash
|
|
383
|
+
${resumeCommand}
|
|
384
|
+
\`\`\``;
|
|
385
|
+
} else if (sessionId) {
|
|
386
|
+
logComment += `you can resume this session using session ID: \`${sessionId}\``;
|
|
387
|
+
} else {
|
|
388
|
+
logComment += 'you can retry the operation.';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const footerNote = isAutoResumeEnabled ? (autoResumeMode === 'restart' ? '*This session was interrupted due to usage limits. The session will automatically restart when the limit resets.*' : '*This session was interrupted due to usage limits. The session will automatically resume when the limit resets.*') : '*This session was interrupted due to usage limits. You can resume once the limit resets.*';
|
|
392
|
+
logComment += `${modelInfoString}
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
<summary>Click to expand execution log (${Math.round(logSizeBytes / 1024)}KB)</summary>
|
|
396
|
+
|
|
397
|
+
\`\`\`
|
|
398
|
+
${logContent}
|
|
399
|
+
\`\`\`
|
|
400
|
+
|
|
401
|
+
</details>
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
${footerNote}`;
|
|
405
|
+
} else if (errorMessage) {
|
|
406
|
+
// Failure log format (non-usage-limit errors)
|
|
407
|
+
logComment = `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
408
|
+
The automated solution draft encountered an error:
|
|
409
|
+
\`\`\`
|
|
410
|
+
${errorMessage}
|
|
411
|
+
\`\`\`${failureAction}${modelInfoString}
|
|
412
|
+
|
|
413
|
+
<details>
|
|
414
|
+
<summary>Click to expand failure log (${Math.round(logSizeBytes / 1024)}KB)</summary>
|
|
415
|
+
|
|
416
|
+
\`\`\`
|
|
417
|
+
${logContent}
|
|
418
|
+
\`\`\`
|
|
419
|
+
|
|
420
|
+
</details>
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
424
|
+
} else if (errorDuringExecution) {
|
|
425
|
+
// Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
|
|
426
|
+
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
427
|
+
logComment = `## ⚠️ ${SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER}
|
|
428
|
+
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
429
|
+
|
|
430
|
+
> **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
431
|
+
|
|
432
|
+
<details>
|
|
433
|
+
<summary>Click to expand solution draft log (${Math.round(logSizeBytes / 1024)}KB)</summary>
|
|
434
|
+
|
|
435
|
+
\`\`\`
|
|
436
|
+
${logContent}
|
|
437
|
+
\`\`\`
|
|
438
|
+
|
|
439
|
+
</details>
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
443
|
+
} else {
|
|
444
|
+
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
445
|
+
// Determine title based on session type (Issue #1152) Issue #1625: Every title variant embeds SOLUTION_DRAFT_LOG_MARKER so the filter in checkForAiCreatedComments matches every variant with a single substring check against the centralized marker constant.
|
|
446
|
+
let title = customTitle;
|
|
447
|
+
let sessionNote = '';
|
|
448
|
+
if (sessionType === 'auto-resume') {
|
|
449
|
+
title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto resume on limit reset)`;
|
|
450
|
+
sessionNote = '\n\n**Note**: This session was automatically resumed after a usage limit reset, with the previous context preserved.';
|
|
451
|
+
} else if (sessionType === 'auto-restart') {
|
|
452
|
+
title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto restart on limit reset)`;
|
|
453
|
+
sessionNote = '\n\n**Note**: This session was automatically restarted after a usage limit reset (fresh start).';
|
|
454
|
+
} else if (sessionType === 'resume') {
|
|
455
|
+
title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (Resumed)`;
|
|
456
|
+
sessionNote = '\n\n**Note**: This session was manually resumed using the --resume flag.';
|
|
457
|
+
}
|
|
458
|
+
logComment = `## ${title}
|
|
459
|
+
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}${sessionNote}
|
|
460
|
+
|
|
461
|
+
<details>
|
|
462
|
+
<summary>Click to expand solution draft log (${Math.round(logSizeBytes / 1024)}KB)</summary>
|
|
463
|
+
|
|
464
|
+
\`\`\`
|
|
465
|
+
${logContent}
|
|
466
|
+
\`\`\`
|
|
467
|
+
|
|
468
|
+
</details>
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
472
|
+
}
|
|
473
|
+
return logComment;
|
|
474
|
+
}
|
|
320
475
|
/** Attaches a log file to a GitHub PR or issue as a comment. Returns true if upload succeeded. */
|
|
321
476
|
export async function attachLogToGitHub(options) {
|
|
322
477
|
const fs = (await use('fs')).promises;
|
|
@@ -351,9 +506,11 @@ export async function attachLogToGitHub(options) {
|
|
|
351
506
|
resultModelUsage = null, // Issue #1454
|
|
352
507
|
budgetStatsData = null, // Issue #1491: budget stats for comment
|
|
353
508
|
failureActionSection = null,
|
|
509
|
+
recordResources = recordResourceSnapshot, // Issue #2189: injectable for tests
|
|
354
510
|
} = options;
|
|
355
511
|
const budgetStats = budgetStatsData ? buildBudgetStatsString(budgetStatsData.tokenUsage, budgetStatsData.subAgentCalls) : '';
|
|
356
512
|
const targetName = targetType === 'pr' ? 'Pull Request' : 'Issue';
|
|
513
|
+
let uploadPhaseEntered = false;
|
|
357
514
|
try {
|
|
358
515
|
// Issue #1212: Check disk space before attempting log upload (100MB minimum)
|
|
359
516
|
try {
|
|
@@ -372,6 +529,12 @@ export async function attachLogToGitHub(options) {
|
|
|
372
529
|
await log(' ⚠️ Log file is empty, skipping upload');
|
|
373
530
|
return false;
|
|
374
531
|
}
|
|
532
|
+
// Issue #2189: sample resources on the way in and on the way out of the
|
|
533
|
+
// upload, tagged with the log size, so a future post-mortem can see the V8
|
|
534
|
+
// heap climbing against its own limit instead of guessing from a machine
|
|
535
|
+
// that still had 10 GB of RAM free.
|
|
536
|
+
uploadPhaseEntered = true;
|
|
537
|
+
await recordResources({ phase: RESOURCE_PHASE_LOG_UPLOAD_START, log, label: `log upload start, ${formatLogSizeForHumans(logStats.size)} log` });
|
|
375
538
|
// Issue #1173: gh-upload-log handles large files; skip inline comment for files > fileMaxSize
|
|
376
539
|
const useLargeFileMode = logStats.size > githubLimits.fileMaxSize;
|
|
377
540
|
if (useLargeFileMode && verbose) {
|
|
@@ -436,148 +599,37 @@ export async function attachLogToGitHub(options) {
|
|
|
436
599
|
}
|
|
437
600
|
}
|
|
438
601
|
}
|
|
439
|
-
// Read and sanitize log content
|
|
440
|
-
const rawLogContent = await fs.readFile(logFile, 'utf8');
|
|
441
|
-
if (verbose) {
|
|
442
|
-
await log(' 🔍 Sanitizing log content to mask GitHub tokens...', { verbose: true });
|
|
443
|
-
}
|
|
444
|
-
let logContent = await sanitizeForPublication(rawLogContent);
|
|
445
|
-
// Escape code blocks in the log content to prevent them from breaking markdown formatting
|
|
446
|
-
if (verbose) {
|
|
447
|
-
await log(' 🔧 Escaping code blocks in log content for safe embedding...', { verbose: true });
|
|
448
|
-
}
|
|
449
|
-
logContent = escapeCodeBlocksInLog(logContent);
|
|
450
602
|
const failureAction = normalizeFailureActionSection(failureActionSection ?? buildIssueFailureActionSection(targetType));
|
|
451
|
-
//
|
|
452
|
-
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
logComment += `\n- **Reset Time**: ${formattedResetTime}`;
|
|
467
|
-
}
|
|
468
|
-
if (sessionId) {
|
|
469
|
-
logComment += `\n- **Session ID**: ${sessionId}`;
|
|
603
|
+
// Issue #2189: choose the publication route from the file size BEFORE touching
|
|
604
|
+
// the bytes. The old order read the log, sanitized it and escaped it — three
|
|
605
|
+
// full-size strings — and only then discovered the comment was far over
|
|
606
|
+
// GitHub's limit and had to be uploaded instead. On a 134 MB transcript that
|
|
607
|
+
// wasted pre-work alone exhausted V8's ~2 GB old space and killed the run
|
|
608
|
+
// ("FATAL ERROR: Reached heap limit"). A log that cannot possibly fit in a
|
|
609
|
+
// comment is now never read into memory at all: it streams straight to
|
|
610
|
+
// gh-upload-log, which sanitizes it block by block.
|
|
611
|
+
const canBuildInlineComment = !useLargeFileMode && logStats.size <= githubLimits.commentMaxSize;
|
|
612
|
+
let logComment = '';
|
|
613
|
+
if (canBuildInlineComment) {
|
|
614
|
+
// Read and sanitize log content
|
|
615
|
+
const rawLogContent = await fs.readFile(logFile, 'utf8');
|
|
616
|
+
if (verbose) {
|
|
617
|
+
await log(' 🔍 Sanitizing log content to mask GitHub tokens...', { verbose: true });
|
|
470
618
|
}
|
|
471
|
-
|
|
472
|
-
//
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
const modeDescription = autoResumeMode === 'restart' ? 'The session will automatically restart (fresh start) when the limit resets.' : 'The session will automatically resume (with context preserved) when the limit resets.';
|
|
476
|
-
logComment += `**Auto-${modeName} is enabled.** ${modeDescription}`;
|
|
477
|
-
} else {
|
|
478
|
-
// Manual resume mode - show CLI commands
|
|
479
|
-
if (limitResetTime) {
|
|
480
|
-
logComment += `Once the limit resets at **${limitResetTime}**, `;
|
|
481
|
-
} else {
|
|
482
|
-
logComment += 'Once the limit resets, ';
|
|
483
|
-
}
|
|
484
|
-
if (resumeCommand) {
|
|
485
|
-
logComment += `you can resume this session by running:
|
|
486
|
-
\`\`\`bash
|
|
487
|
-
${resumeCommand}
|
|
488
|
-
\`\`\``;
|
|
489
|
-
} else if (sessionId) {
|
|
490
|
-
logComment += `you can resume this session using session ID: \`${sessionId}\``;
|
|
491
|
-
} else {
|
|
492
|
-
logComment += 'you can retry the operation.';
|
|
493
|
-
}
|
|
619
|
+
let logContent = await sanitizeForPublication(rawLogContent);
|
|
620
|
+
// Escape code blocks in the log content to prevent them from breaking markdown formatting
|
|
621
|
+
if (verbose) {
|
|
622
|
+
await log(' 🔧 Escaping code blocks in log content for safe embedding...', { verbose: true });
|
|
494
623
|
}
|
|
495
|
-
|
|
496
|
-
logComment
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
<summary>Click to expand execution log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
500
|
-
|
|
501
|
-
\`\`\`
|
|
502
|
-
${logContent}
|
|
503
|
-
\`\`\`
|
|
504
|
-
|
|
505
|
-
</details>
|
|
506
|
-
|
|
507
|
-
---
|
|
508
|
-
${footerNote}`;
|
|
509
|
-
} else if (errorMessage) {
|
|
510
|
-
// Failure log format (non-usage-limit errors)
|
|
511
|
-
logComment = `## 🚨 ${SOLUTION_DRAFT_FAILED_MARKER}
|
|
512
|
-
The automated solution draft encountered an error:
|
|
513
|
-
\`\`\`
|
|
514
|
-
${errorMessage}
|
|
515
|
-
\`\`\`${failureAction}${modelInfoString}
|
|
516
|
-
|
|
517
|
-
<details>
|
|
518
|
-
<summary>Click to expand failure log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
519
|
-
|
|
520
|
-
\`\`\`
|
|
521
|
-
${logContent}
|
|
522
|
-
\`\`\`
|
|
523
|
-
|
|
524
|
-
</details>
|
|
525
|
-
|
|
526
|
-
---
|
|
527
|
-
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
528
|
-
} else if (errorDuringExecution) {
|
|
529
|
-
// Issue #1088: "Finished with errors" format - work may have been completed but errors occurred
|
|
530
|
-
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
531
|
-
logComment = `## ⚠️ ${SOLUTION_DRAFT_FINISHED_WITH_ERRORS_MARKER}
|
|
532
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}
|
|
533
|
-
|
|
534
|
-
> **Note**: The session encountered errors during execution, but some work may have been completed. Please review the changes carefully.
|
|
535
|
-
|
|
536
|
-
<details>
|
|
537
|
-
<summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
538
|
-
|
|
539
|
-
\`\`\`
|
|
540
|
-
${logContent}
|
|
541
|
-
\`\`\`
|
|
542
|
-
|
|
543
|
-
</details>
|
|
544
|
-
|
|
545
|
-
---
|
|
546
|
-
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
547
|
-
} else {
|
|
548
|
-
const costInfo = buildCostInfoString(totalCostUSD, anthropicTotalCostUSD, pricingInfo, { includeTokenUsage: !budgetStats });
|
|
549
|
-
// Determine title based on session type (Issue #1152) Issue #1625: Every title variant embeds SOLUTION_DRAFT_LOG_MARKER so the filter in checkForAiCreatedComments matches every variant with a single substring check against the centralized marker constant.
|
|
550
|
-
let title = customTitle;
|
|
551
|
-
let sessionNote = '';
|
|
552
|
-
if (sessionType === 'auto-resume') {
|
|
553
|
-
title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto resume on limit reset)`;
|
|
554
|
-
sessionNote = '\n\n**Note**: This session was automatically resumed after a usage limit reset, with the previous context preserved.';
|
|
555
|
-
} else if (sessionType === 'auto-restart') {
|
|
556
|
-
title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (auto restart on limit reset)`;
|
|
557
|
-
sessionNote = '\n\n**Note**: This session was automatically restarted after a usage limit reset (fresh start).';
|
|
558
|
-
} else if (sessionType === 'resume') {
|
|
559
|
-
title = `🔄 ${SOLUTION_DRAFT_LOG_MARKER} (Resumed)`;
|
|
560
|
-
sessionNote = '\n\n**Note**: This session was manually resumed using the --resume flag.';
|
|
561
|
-
}
|
|
562
|
-
logComment = `## ${title}
|
|
563
|
-
This log file contains the complete execution trace of the AI ${targetType === 'pr' ? 'solution draft' : 'analysis'} process.${costInfo}${budgetStats}${modelInfoString}${sessionNote}
|
|
564
|
-
|
|
565
|
-
<details>
|
|
566
|
-
<summary>Click to expand solution draft log (${Math.round(logStats.size / 1024)}KB)</summary>
|
|
567
|
-
|
|
568
|
-
\`\`\`
|
|
569
|
-
${logContent}
|
|
570
|
-
\`\`\`
|
|
571
|
-
|
|
572
|
-
</details>
|
|
573
|
-
|
|
574
|
-
---
|
|
575
|
-
*${NOW_WORKING_SESSION_IS_ENDED_MARKER}, feel free to review and add any feedback on the solution draft.*`;
|
|
624
|
+
logContent = escapeCodeBlocksInLog(logContent);
|
|
625
|
+
logComment = buildInlineLogComment({ logContent, logSizeBytes: logStats.size, targetType, customTitle, sessionType, sessionId, errorMessage, errorDuringExecution, isUsageLimit, limitResetTime, toolName, resumeCommand, isAutoResumeEnabled, autoResumeMode, modelInfoString, budgetStats, totalCostUSD, anthropicTotalCostUSD, pricingInfo, failureAction });
|
|
626
|
+
} else if (verbose) {
|
|
627
|
+
await log(` ⏭️ Log is ${formatLogSizeForHumans(logStats.size)} — skipping inline comment construction, the log goes straight to gh-upload-log`, { verbose: true });
|
|
576
628
|
}
|
|
577
629
|
// Check GitHub comment size limit or large file mode Issue #1173: Also use gh-upload-log for large files, not just long comments
|
|
578
|
-
if (
|
|
579
|
-
if (
|
|
580
|
-
await log(` 📁 Log file too large for inline comment (${
|
|
630
|
+
if (!canBuildInlineComment || logComment.length > githubLimits.commentMaxSize) {
|
|
631
|
+
if (!canBuildInlineComment) {
|
|
632
|
+
await log(` 📁 Log file too large for inline comment (${formatLogSizeForHumans(logStats.size)}), using gh-upload-log`);
|
|
581
633
|
} else {
|
|
582
634
|
// Issue #2160: this is the expected route for a long log, not a problem — the upload
|
|
583
635
|
// below handles it. Reporting it as a warning made every normal run look degraded.
|
|
@@ -607,23 +659,20 @@ ${logContent}
|
|
|
607
659
|
isPublicRepo = false;
|
|
608
660
|
await log(' ⚠️ Could not determine repository visibility, defaulting to private for safety');
|
|
609
661
|
}
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
//
|
|
613
|
-
|
|
662
|
+
// Issue #2189: hand the raw log path to the uploader and let it stream the
|
|
663
|
+
// sanitize into its own private temp file. This used to read the whole log
|
|
664
|
+
// a second time and write a sanitized copy here, after which
|
|
665
|
+
// uploadLogWithGhUploadLog read and sanitized that copy a *third* time —
|
|
666
|
+
// which is why the incident log shows "🔒 Sanitized 591 secrets" twice.
|
|
667
|
+
// There is now exactly one sanitize pass, and it never buffers the file.
|
|
614
668
|
// Use gh-upload-log default auto mode and shared repository fallback.
|
|
615
669
|
const uploadDescription = `Solution draft log for https://github.com/${owner}/${repo}/${targetType === 'pr' ? 'pull' : 'issues'}/${targetNumber}`;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
verbose,
|
|
623
|
-
});
|
|
624
|
-
} finally {
|
|
625
|
-
await fs.unlink(tempLogFile).catch(() => {});
|
|
626
|
-
}
|
|
670
|
+
const uploadResult = await uploadLogWithGhUploadLog({
|
|
671
|
+
logFile,
|
|
672
|
+
isPublic: isPublicRepo,
|
|
673
|
+
description: uploadDescription,
|
|
674
|
+
verbose,
|
|
675
|
+
});
|
|
627
676
|
if (uploadResult.success) {
|
|
628
677
|
// Use rawUrl for direct file access (single chunk) or url for repository (multiple chunks) Requirements: 1 chunk = direct raw link, >1 chunks = repo link Private repository raw URLs can contain short-lived tokens, so keep private uploads on the stable repository/tree page URL.
|
|
629
678
|
const logUrl = selectLogUploadUrl({ uploadResult, isPublicRepo });
|
|
@@ -789,6 +838,17 @@ ${sessionNote}
|
|
|
789
838
|
const msg = isENOSPC(uploadError) ? 'ENOSPC: No space left on device during log upload. Free disk space and retry.' : `Error uploading log file: ${uploadError.message}`;
|
|
790
839
|
await log(` ❌ ${msg}`);
|
|
791
840
|
return false;
|
|
841
|
+
} finally {
|
|
842
|
+
// Issue #2189: the closing sample must run on every exit path, including the
|
|
843
|
+
// failure ones — an upload that died half way through is exactly the case
|
|
844
|
+
// whose memory profile we need.
|
|
845
|
+
if (uploadPhaseEntered) {
|
|
846
|
+
try {
|
|
847
|
+
await recordResources({ phase: RESOURCE_PHASE_LOG_UPLOAD_END, log, label: 'log upload end' });
|
|
848
|
+
} catch {
|
|
849
|
+
/* telemetry must never change the outcome of the upload */
|
|
850
|
+
}
|
|
851
|
+
}
|
|
792
852
|
}
|
|
793
853
|
}
|
|
794
854
|
/**
|