From d9951f8be706fb54263293350f46e686f7a4cbae Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 14 Sep 2026 15:27:29 -0400 Subject: [PATCH] fix: classify capped artifact probe candidates --- api/src/job.ts | 56 +++++++++++++++++++++++++++++++++--------- api/src/walker.test.ts | 43 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 12 deletions(-) diff --git a/api/src/job.ts b/api/src/job.ts index 27604d29..a5b7ded2 100644 --- a/api/src/job.ts +++ b/api/src/job.ts @@ -2208,7 +2208,7 @@ export class Job { state = this.truncationProbeState, probeDepth = 0, rootPath = path.relative(this.submissionDir, dir) || '.', - respectSessionSuppression = false, + isOutputCapProbe = false, ): Promise { /* The state is shared by every probe in this job. Once exhausted, return * conservatively before opening yet another capped sibling directory. */ @@ -2246,17 +2246,51 @@ export class Job { if (entry.name !== DIRKEEP && !isSupportedOutputFilename(entry.name)) continue; const existingFile = inputByName.get(relativePath); const inputFileInfo = this.inputFileHashes.get(relativePath); + let capProbeStat: fs.Stats | undefined; + if (isOutputCapProbe) { + const pathShapeError = checkPathShape(relativePath); + if (pathShapeError) { + this.recordArtifactTruncation( + pathShapeError.includes('nesting depth') ? 'depth' : 'path', + relativePath, + ); + continue; + } + try { + capProbeStat = await fsp.lstat(fullPath); + if (!capProbeStat.isFile()) continue; + } catch (err) { + this.log.debug({ path: relativePath, err }, 'walkDir: failed during cap-probe stat'); + this.recordArtifactTruncation('unreadable', relativePath); + continue; + } + if (capProbeStat.size > this.runtime.max_file_size) { + /* Match handleRegularFile's one exception: an unchanged inline + * entrypoint is request input rather than an oversized output. */ + if (!inputFileInfo || existingFile?.id != null || relativePath !== this.entryPointName) { + this.recordArtifactTruncation('size', relativePath); + continue; + } + if (capProbeStat.size > state.remainingHashBytes) return rootPath; + state.remainingHashBytes -= capProbeStat.size; + try { + if (await this.computeFileHash(fullPath, true) === inputFileInfo.hash) continue; + } catch (err) { + this.log.debug({ path: relativePath, err }, 'walkDir: failed during oversized entrypoint cap probe'); + } + this.recordArtifactTruncation('size', relativePath); + continue; + } + } if ( - respectSessionSuppression + isOutputCapProbe && relativePath === this.entryPointName && existingFile?.id == null && inputFileInfo ) { try { - const st = await fsp.lstat(fullPath); - if (!st.isFile()) continue; - if (st.size > state.remainingHashBytes) return rootPath; - state.remainingHashBytes -= st.size; + if (capProbeStat!.size > state.remainingHashBytes) return rootPath; + state.remainingHashBytes -= capProbeStat!.size; if (await this.computeFileHash(fullPath, true) === inputFileInfo.hash) continue; } catch (err) { this.log.debug({ path: relativePath, err }, 'walkDir: failed during entrypoint cap probe'); @@ -2270,13 +2304,11 @@ export class Job { * so the bounded cap probe must do the same or it reports a false * max_files warning. Current-request inputs remain reportable: they * would otherwise have been echoed into this response. */ - if (respectSessionSuppression && this.session && !existingFile) { + if (isOutputCapProbe && this.session && !existingFile) { if (this.session.isPrimedReadOnly(relativePath)) continue; try { - const st = await fsp.lstat(fullPath); - if (!st.isFile()) continue; - if (st.size > state.remainingHashBytes) return rootPath; - state.remainingHashBytes -= st.size; + if (capProbeStat!.size > state.remainingHashBytes) return rootPath; + state.remainingHashBytes -= capProbeStat!.size; const hash = await this.computeFileHash(fullPath, true); if (this.session.isSurfaced(relativePath, hash)) continue; if ( @@ -2303,7 +2335,7 @@ export class Job { state, probeDepth + 1, rootPath, - respectSessionSuppression, + isOutputCapProbe, ); if (nested) return nested; } diff --git a/api/src/walker.test.ts b/api/src/walker.test.ts index 543505a9..ba6f4af3 100644 --- a/api/src/walker.test.ts +++ b/api/src/walker.test.ts @@ -1021,6 +1021,49 @@ describe('walkDir / artifact truncation details', () => { }); }); + it('classifies an oversized supported file by size when the output cap is full', async () => { + await fsp.writeFile(path.join(tmpDir, 'oversized.txt'), 'too large'); + const internals = asInternals(makeJob({ maxFileSize: 3 })); + internals.submissionDir = tmpDir; + internals.generatedFiles = Array.from({ length: config.max_output_files }, (_, i) => ({ + id: `id-${i}`, + name: `file-${i}.txt`, + path: path.join(tmpDir, `file-${i}.txt`), + })); + + await internals.walkDir(tmpDir, 0, new Map()); + + expect(internals.artifactTruncation).toEqual({ + code: 'artifact_truncated', + reasons: { size: 1 }, + skipped: ['oversized.txt'], + skipped_count: 1, + }); + }); + + it('classifies an overlong supported path by path when the output cap is full', async () => { + const directory = 'a'.repeat(200); + const filename = path.join(directory, `${'b'.repeat(60)}.txt`); + await fsp.mkdir(path.join(tmpDir, directory)); + await fsp.writeFile(path.join(tmpDir, filename), 'output'); + const internals = asInternals(makeJob()); + internals.submissionDir = tmpDir; + internals.generatedFiles = Array.from({ length: config.max_output_files }, (_, i) => ({ + id: `id-${i}`, + name: `file-${i}.txt`, + path: path.join(tmpDir, `file-${i}.txt`), + })); + + await internals.walkDir(tmpDir, 0, new Map()); + + expect(internals.artifactTruncation).toEqual({ + code: 'artifact_truncated', + reasons: { path: 1 }, + skipped: [filename], + skipped_count: 1, + }); + }); + it('does not hash ordinary oversized files in session mode', async () => { await fsp.writeFile(path.join(tmpDir, 'large.txt'), 'too large'); const session = new SessionWorkspace({ runtimeSessionId: 'rt_large' });