fix(opencode): skip apply_patch diffs for binary and large deletes
Stops apply_patch from running a sandbox out of memory when it deletes a large binary file. A delete of a binary file, or of any file over 1 MiB, now records only its size in tool metadata instead of a full diff of its contents.
Context: REPL-33048 (found in the media-to-Assets eval). Upstream report: anomalyco/opencode#41733, closed unfixed as "V1". This ports #41734, which was closed unmerged. Write-up: Large File Delete Crash.
Symptom
A GPT-model agent asked to move a 30 MiB video out of public/ deleted it with one apply_patch (*** Delete File: public/videos/brand-film.mp4). The sandbox's agent process was then killed and restarted mid-turn ([opencode-exit] {"signal":9,"oomKill":1,"memPeakMib":4096}), and the user saw "The agent restarted before it could finish this turn." It happened in 6 of 6 eval runs of that scenario. Only GPT models are affected, because only they get apply_patch (tool/registry.ts). Production last week: 2,503 of 2,504 apply_patch calls were GPT-5.6.
Cause
The delete branch read the whole file, decoded it as UTF-8, and built createTwoFilesPatch(content, ""). That diff went into tool metadata twice (metadata.diff and metadata.files[].patch). Decoding and diffing are cheap, about 200 MB for 30 MiB. Serializing the tool part is not: every control byte in a binary becomes a six-character \u00XX escape, so one JSON copy of the part was 347 MiB, and producing it took about 2 GB of memory. A couple of copies (the stored part, the event stream) exceed a 4 GiB sandbox. The model never sees any of this; its output is just D <path>.
Fix
In the delete case, stat the file first. If it is over 1 MiB, don't read it. If it is smaller, read it and run the read tool's existing isBinaryFile check. Binary or oversized deletes get a two-line marker diff, Index: <path> and Binary or oversized file deleted (<bytes> bytes; content diff omitted), with deletions: 0. Small text deletes keep their full diff. isBinaryFile moves out of read.ts into util/binary.ts so both tools share it; its behavior is unchanged. Missing files and directories still fail.
V2 has the same code path (packages/core/src/tool/plugin/patch.ts delete → fileDiff), so this guard needs re-applying there during the V2 migration.
How did you verify your code works?
Unit tests (bun test test/tool/apply_patch.test.ts test/tool/read.test.ts): 68 pass, and bun run typecheck passes.
- One new test deletes a 512 KiB
.mp4(NUL bytes, size under the cap, so the content check catches it) and a text file just over 1 MiB in one patch. It asserts the metadata stays under 1 KB withdeletions: 0. It fails ondevwithout this change. - The existing add/update/delete test now also asserts the small text delete keeps its line count.
End to end on a real Daytona sandbox (staging eval stack ev-7ac634c081). Linux x64 builds of this branch were installed over the sandbox's opencode. The publish run used commit 3f96871; the 30 MiB delete was rerun on the final commit ab9a666 (sha256 8d8a5c60…).
| Run | Before (v1.18.32-9) | This branch |
|---|---|---|
GPT agent deletes the 30 MiB video with apply_patch | agent 0.7 → 3.4 GiB in 3 s, OOM-killed, restarted | same pid, peak 0.85 GiB, file deleted, tool metadata 528 bytes |
GPT agent deletes a 2-line text file with apply_patch | not run | full diff kept (-alpha line, -beta line) |
Real turn through the stack coordinator: "Publish my site please." with the 30 MiB video in public/ and the publish gate installed | restarted mid-fix in 6 of 6 eval runs | gate refused → upload to Assets → one apply_patch (update BrandFilm.tsx + delete the mp4) → publish succeeded; same agent pid across all 477 memory samples (peak 0.93 GiB); live site returns 200 and serves the video from Assets |
Type of change
- Bug fix
Checklist
- I have tested my changes locally
- I have not included unrelated changes in this PR
🤖 Generated with Claude Code