Skip to content

fix(collect): stream large CUA artifacts and fail on dropped deliverables - #53

Merged
williamloo-scale merged 2 commits into
mainfrom
wloo/collect-artifacts-large-file
Oct 3, 2026
Merged

williamloo-scale merged 2 commits into
mainfrom
wloo/collect-artifacts-large-file

Conversation

@williamloo-scale

@williamloo-scale williamloo-scale commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

collect_artifacts reads CUA desktop-VM files with a single cua_get_file call that buffers the whole file as one base64 body in one gateway /step response. That can't carry a large file within one request, and every per-file failure was caught and downgraded to a logger.warning while the step still "succeeded" on the other files — so a large deliverable silently vanished from the result artifact.

Confirmed in prod on run aeh-1790880504327 (file ~/Desktop/sintel_braided_assembly_v2.mp4, ~500MB):

  • Collecting 12 artifacts … via controller → Collected 11/12
  • Failed to read …/sintel_braided_assembly_v2.mp4 via controller: logged exactly 600s after the read started — the _controller_call timeout=600 firing (httpx ReadTimeout, empty string). A retry attempt died with a 503 from the sandbox /step mid-transfer instead.
  • The other seven .mp4s (18–52MB each) collected fine in 1–9s — a size/transport ceiling, not a cap.

Fix (contained, client-side)

  1. Stream large files. Files > 16MB are read off the VM in 8MB byte ranges via cua_bash (tail -c +N | head -c M | base64, portable across the Ubuntu and macOS desktop VMs — no GNU dd iflag), decoding each range as it arrives. No single controller call moves the whole file. A short read (sum ≠ wc -c size) fails loudly rather than uploading a truncated artifact. Files ≤16MB keep the existing single-shot path.
  2. Fail on dropped deliverables. A genuinely-absent listed file stays a lenient skip (FileNotFoundError); a file that exists but fails to read or upload now fails the step (retryable) instead of being dropped with only a warning. Applies to the controller and the agent / VM-host / sandbox-container paths.
  3. A size-probe error is a hard failure, not a missing-file skip. wc -c / stat return -1 only for a genuinely-absent file (stderr "No such file"); a probe that times out or otherwise errors raises, so an existing deliverable is never silently dropped by a flaky size check. The agent-path size check sits inside the guarded block so the provider still closes on failure.

Tests

uv run --extra dev pytest tst/unit/task_step/ → 1385 passed. New/updated coverage in test_collect_artifacts.py: chunked reassembly (incl. base64 76-col wrapping), short-read → fatal, absent → FileNotFoundError vs present-but-failed → fatal, zero-byte file present, single-shot for small files, size-probe transport/wc/stat errors → fatal.

Notes / trade-offs

  • Adds one cheap wc -c round-trip per file (to size it before deciding single-shot vs chunked, and to tell absent from present-but-failed). Minor latency for correctness; mirrors the agent-container path, which already stats first.
  • Large files are reassembled in memory before upload (not streamed to a temp file) to preserve the existing _controller_get_file -> bytes seam; peak memory ≈ file size, fine for worker sizing.
  • The server-side cua_get_file default 90s timeout lives in scaleapi/packages/agentenvhub-sdk; with chunking capping the single-shot path at 16MB it's non-blocking, so a 300s bump there is an optional separate follow-up.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

The PR is not ready to merge because an existing unreadable file can still be mistaken for a missing one.

Fix All in CursorFindings

  1. P1 Existing files get skipped ▶
Fix with agent prompt
### Issue 1
src/agent_env/task_step/task_steps/collect_artifacts.py:undefined-198
When `stat` cannot read a file because of a permission or I/O error, it can still say `cannot stat`. `_looks_absent` treats that phrase as proof the file is missing. The collector then skips an existing deliverable instead of failing the step. Match the missing-file error, not the general `stat` prefix.

```suggestion
    return "No such file" in stderr
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR collects large CUA files in 8 MB ranges instead of sending each file in one gateway response. It also makes collection fail when an existing deliverable cannot be sized, read, or uploaded.

  • Small files keep the single-call read path; large files are staged locally before upload.
  • Missing files are skipped, while failures to collect existing files fail the step.
  • Unit tests cover ranged reads, short reads, and missing files versus failed reads.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[Size file on VM] --> B{Over 16 MB?}
    B -- No --> C[Download in one call]
    B -- Yes --> D[Read chunks to temp file]
    C --> E[Upload temp file]
    D --> E
    E --> F[Remove temp file]
Loading

Reviews (2) · Last reviewed commit: "fix(collect): stream chunks to disk and ..."

…bles

collect_artifacts read CUA desktop-VM files with a single cua_get_file call that
buffers the whole file as one base64 body in one gateway response. A ~500MB
deliverable blew past the step's 600s controller timeout (observed: run
aeh-1790880504327, sintel_braided_assembly_v2.mp4, exactly 600s to the
empty-error ReadTimeout), and because every per-file failure was caught and
downgraded to a warning while the step still "succeeded" on the other files, the
big file silently vanished from the result artifact.

- Stream files larger than 16MB off the VM in 8MB byte ranges via cua_bash
  (`tail -c +N | head -c M | base64`, portable across the Ubuntu and macOS
  desktop VMs), decoding each range to the output — no single call moves the
  whole file. A short read fails loudly instead of uploading a partial artifact.
- Distinguish absent from present-but-untransferable: a listed file that isn't
  on the VM stays a lenient skip, but a file that EXISTS and fails to read or
  upload now fails the step (retryable) instead of being dropped with a warning.
  Applies to the controller and the agent/VM-host/sandbox-container paths.
- A size-probe (`wc -c` / `stat`) error is a hard failure, NOT a missing-file
  skip: -1 means genuinely absent only (stderr "No such file"); a probe that
  times out or otherwise errors raises so an existing deliverable is never
  silently dropped. The agent-path size check sits inside the guarded block so
  the provider still closes on failure.

Unit tests cover chunked reassembly (with base64 wrapping), the short-read
guard, absent -> FileNotFoundError vs present-but-failed -> fatal, zero-byte
files, and size-probe errors -> fatal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@williamloo-scale
williamloo-scale requested a review from a team as a code owner October 2, 2026 23:17
Comment thread src/agent_env/task_step/task_steps/collect_artifacts.py Outdated
Comment thread src/agent_env/task_step/task_steps/collect_artifacts.py Outdated
Addresses Greptile review on #53.

P2: the chunked reader accumulated every decoded chunk in a bytearray and copied
the whole file with bytes(buf) before writing it out, so a large collect still
peaked at ~2x the file in memory. _controller_get_file now downloads to a temp
file and returns its path — large files stream range-by-range straight to disk
(bounded to one chunk), small files decode to the same file — and the controller
loop uploads that file via _upload_file (replacing _write_and_upload). Memory no
longer scales with file size.

P1: _looks_absent matched "cannot stat", but a permission/IO error also says
"cannot stat" — that would treat an unreadable existing file as missing and skip
it. Match "No such file" only, so a non-missing stat/wc error stays a hard
failure.

Tests updated for the path-returning seam (stubs hand back a staged temp file);
added an end-to-end check that a large file is streamed to disk and uploaded from
its path with bytes intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@williamloo-scale
williamloo-scale enabled auto-merge (squash) October 3, 2026 05:00
@williamloo-scale
williamloo-scale merged commit 0dbbfcb into main Oct 3, 2026
13 checks passed
@williamloo-scale
williamloo-scale deleted the wloo/collect-artifacts-large-file branch October 3, 2026 08:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants