Files
orca/src/shared/check-job-log-tail-slice.ts
d04b05b5c8 Detach retained CI and terminal tails from oversized strings (#20960)
* fix(memory): detach retained CI and terminal tails from oversized strings

* fix(terminal): detach retained error and reattach string slices

* fix(terminal): release oversized recent-output backing strings

* fix(terminal): release backing strings held by PTY detectors

* fix(memory): own bounded Claude background task labels

* fix: detach retained terminal mode scan tails

* fix: own retained plugin worker output strings

* fix: own incomplete OSC 133 carry strings

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
2026-09-17 20:34:19 -07:00

89 lines
3.7 KiB
TypeScript

import {
clampUtf8TextTail,
getUtf8ByteLength,
isUtf8ByteLengthWithinLimit
} from './utf8-byte-limits'
import { ownRetainedString } from './own-retained-string'
export const PR_CHECK_LOG_TAIL_LINES = 200
export const PR_CHECK_LOG_TAIL_RECENT_LINES = 100
export const PR_CHECK_LOG_TAIL_BYTES = 16 * 1024
export const PR_CHECK_LOG_TAIL_ERROR_CONTEXT_LINES = 2
export const PR_CHECK_LOG_TAIL_MAX_EARLIER_LINES = 30
export const PR_CHECK_LOG_TAIL_EARLIER_SEPARATOR = '… earlier errors …'
// Why: install/cache steps can flood the tail; pull a small window around earlier
// error markers so failures remain visible without downloading the full job log.
const ERROR_LINE_PATTERN =
/(?:##\[error\]|::error::|::error\b|\berror:|FAILED|exit code|ENOENT|EACCES|panic:|AssertionError)/i
function applyLogTailByteCap(text: string): string {
if (isUtf8ByteLengthWithinLimit(text, PR_CHECK_LOG_TAIL_BYTES)) {
return text
}
return clampUtf8TextTail(text, PR_CHECK_LOG_TAIL_BYTES).text
}
function joinLogExcerptWithByteCap(prefixLines: string[], recentLines: string[]): string {
const prefix = prefixLines.join('\n')
// UTF-16 length is a lower bound; oversized prefixes need no exact byte count.
const prefixByteLength =
prefix.length >= PR_CHECK_LOG_TAIL_BYTES ? PR_CHECK_LOG_TAIL_BYTES : getUtf8ByteLength(prefix)
if (prefixByteLength >= PR_CHECK_LOG_TAIL_BYTES) {
return clampUtf8TextTail(prefix, PR_CHECK_LOG_TAIL_BYTES).text
}
const separator = prefix.length > 0 && recentLines.length > 0 ? '\n' : ''
const recentBudget = PR_CHECK_LOG_TAIL_BYTES - prefixByteLength - getUtf8ByteLength(separator)
const recentTail = clampUtf8TextTail(recentLines.join('\n'), recentBudget).text
return `${prefix}${separator}${recentTail}`
}
function collectEarlierErrorLineIndexes(lines: string[], recentStart: number): number[] {
const indexes = new Set<number>()
// Newest-first errors and descending windows add the newest unique indexes first.
for (let index = recentStart - 1; index >= 0; index -= 1) {
if (!ERROR_LINE_PATTERN.test(lines[index] ?? '')) {
continue
}
const contextStart = Math.max(0, index - PR_CHECK_LOG_TAIL_ERROR_CONTEXT_LINES)
const contextEnd = Math.min(recentStart - 1, index + PR_CHECK_LOG_TAIL_ERROR_CONTEXT_LINES)
for (let contextIndex = contextEnd; contextIndex >= contextStart; contextIndex -= 1) {
indexes.add(contextIndex)
if (indexes.size === PR_CHECK_LOG_TAIL_MAX_EARLIER_LINES) {
return [...indexes].sort((left, right) => left - right)
}
}
}
return [...indexes].sort((left, right) => left - right)
}
function buildCheckLogTail(logText: string): string {
const lines = logText.split(/\r?\n/)
const recentStart = Math.max(0, lines.length - PR_CHECK_LOG_TAIL_RECENT_LINES)
const recentLines = lines.slice(recentStart)
if (recentStart === 0) {
return applyLogTailByteCap(lines.join('\n'))
}
const earlierIndexes = collectEarlierErrorLineIndexes(lines, recentStart)
if (earlierIndexes.length === 0) {
return applyLogTailByteCap(lines.slice(-PR_CHECK_LOG_TAIL_LINES).join('\n'))
}
const cappedEarlierIndexes = earlierIndexes.slice(-PR_CHECK_LOG_TAIL_MAX_EARLIER_LINES)
const earlierLines = cappedEarlierIndexes.map((index) => lines[index] ?? '')
// Why: if the recent tail alone exceeds the byte cap, keep the earlier error
// context visible instead of truncating it back out of the combined excerpt.
return joinLogExcerptWithByteCap(
[...earlierLines, PR_CHECK_LOG_TAIL_EARLIER_SEPARATOR],
recentLines
)
}
export function sliceCheckLogTail(logText: string): string {
// Cached excerpts must not pin the downloaded log behind a small V8 slice.
return ownRetainedString(buildCheckLogTail(logText))
}