mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 22:56:58 +01:00
## Thinking Path - Per-turn remote sandboxes are the default, so every remote turn uploads the workspace into a fresh Daytona sandbox. - The Daytona plugin compresses large uploads with zstd, but only when the sandbox has a `zstd` binary. Without one, the upload goes raw: 220 MB for the paperclip seed. - The default Paperclip runner image has zstd, so it is already covered. A sandbox image without zstd is not: for example a user-supplied `config.snapshot` built on plain `debian:bookworm`. Such an image still has gzip. - This adds gzip as a fallback codec for those images. It is a robustness fallback, not a speedup for the default image. ## Linked Issues or Issue Description Refs #20100 **What happened?** On a Daytona sandbox image without a `zstd` binary, every remote turn uploads the workspace seed uncompressed. Measured live with the paperclip seed: raw takes 18.6–28.8 s, against 6.1–16.0 s compressed. **Steps to reproduce** 1. Use a Daytona snapshot whose image has gzip but no zstd. 2. Run a remote task on a large repository and inspect the `transfer` span: there is no `transfer.compression.codec` attribute, and the full raw size is uploaded. **Expected behavior** Large workspace tarballs are compressed in transit whenever the sandbox can decompress them. ## What Changed - `packages/plugins/sandbox-providers/daytona/src/file-sync.ts`: - The existing availability probe (the same round trip as `mkdir`) also reports `gzip`. - Codec choice: zstd when the sandbox has it and Node supports it (unchanged), otherwise gzip, otherwise raw (unchanged). - gzip runs at level 1. On the 220 MB paperclip seed it produces 69.6 MB in 1.26 s on the host; zstd -3 produces 57.6 MB in 0.54 s. - In the sandbox, gzip decompresses with `gzip -dc -- <scratch> > <raw scratch>`. It reuses the existing explicit `chmod`, atomic `mv -f`, failure sweep, private host temp directory, and minimum-saving fallback. - A failed or partial decompress exits 49, the raw scratch is never promoted, and every scratch file is swept. The workspace sentinel and `baselineSha256` checks still run before `turn.start`. - The `transfer.compression.codec` span attribute is now `zstd` or `gzip` (still a closed set). - Tests in `file-sync.test.ts` (red on master): - A real shell promotion on a sandbox that reports no zstd: the upload is `.gz` and under half the size, `gzip -dc` runs, and the target is byte-identical with mode 0600 and no scratch left. - A truncated gzip upload fails the sync, never creates the target, and leaves no scratch. - Codec preference: zstd when both are present, gzip when only gzip is, raw when neither is. ## Verification - `npx vitest run` in `packages/plugins/sandbox-providers/daytona`: all pass. The existing zstd suite runs on this host because it has zstd. - `tsc --noEmit -p packages/plugins/sandbox-providers/daytona`: clean. - Live Daytona run on the runner image, 2 vCPU, 220 MB seed, through the real `performSyncIn` with each codec forced through the probe. The sha256 matched every time. | Codec | Times | |---|---| | raw | 28.8 s, 18.6 s, 20.2 s, 22.8 s | | zstd | 6.9 s, 6.1 s, 16.0 s, 8.5 s | | gzip | 9.8 s, 16.8 s | Network variance is large. Both codecs roughly halve the raw upload or better. ## Risks - **Host CPU:** about 1.3 s of streamed gzip per large upload, on the libuv thread pool, and only when the sandbox lacks zstd. Files under 8 MB, or that save less than 10%, still go raw. - **No compressor at all:** a sandbox without gzip or zstd uploads raw, as today. - **Probe caching:** the probe runs on every sync, on the `mkdir` round trip it already makes, so it adds no latency. It stays uncached, following the existing design's rule that one principal's observation is never reused for another. - **Observability:** one new value, `gzip`, in an existing closed-set span attribute. The endpoint gate is unchanged. ## Model Used Claude Opus 5.5 (`claude-opus-5-5`, Anthropic), 1M context window, tool use and code execution, via Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>