Files
PaperClipAI/packages
Devin FoleyandClaude Opus 5.5 522c653d56 fix(runner): retry stalled Pi Node archive downloads (#18126)
<!-- entire-trail-link-start -->
https://entire.com/gh/paperclipai/-/trails/230/changes/paperclip/230
<!-- entire-trail-link-end -->

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The master Docker image contains a runner provider pack, and the
pack contains the Pi agent runtime
> - The Pi materializer downloads a hash-pinned Node archive from
nodejs.org during the image build
> - That download has one 60 s timeout and no retry, so one stalled
transfer stops the full amd64 image build
> - Master Docker builds failed this way two times in two days, and the
failure did not identify the request
> - This pull request retries only transport failures of that download
and keeps every security gate
> - The benefit is fewer red master image builds from short nodejs.org
stalls, and a clear error when all attempts fail

## Linked Issues or Issue Description

No public issue exists. This is a bug report.

**What happened?**
The `Docker` workflow on `master` failed in the `build-and-push
(linux/amd64)` job at `Dockerfile:163` (`runner-provider-pack`).
`build-provider-pack.mjs` stopped with an uncaught `DOMException
[TimeoutError]: The operation was aborted due to timeout`. The arm64 job
of the same commit passed. The next master build passed without a code
change.
- https://github.com/paperclipai/paperclip/actions/runs/40686298026
(2026-10-28)
- https://github.com/paperclipai/paperclip/actions/runs/40808852599
(2026-10-29)

**Steps to reproduce**
1. Run `node
packages/paperclip-runner/scripts/materialize-pi-distribution.mjs
/tmp/pi-out` on linux-x64.
2. Make the transfer from nodejs.org stall for more than 60 s.
3. The script stops at once with an anonymous `TimeoutError`.

**Expected behavior**
A short network stall does not stop the build. If all attempts fail, the
error identifies the Pi Node download.

**Evidence**
In both failed jobs, the uncaught timeout came about 60 s after the last
provider-pack output line (61.18 s → 121.6 s, and 38.72 s → 99.15 s).
The Pi Node archive fetch is the only provider download with a 60 s
timeout. The Cursor and Copilot downloads use 120–180 s.

## What Changed

- `materialize-pi-distribution.mjs`: add `downloadPinnedPiNodeArchive`.
It tries the download up to 3 times. Each attempt has a new 60 s
timeout. The timeout covers the headers and the full body. The backoff
is 5 s, then 10 s.
- Only transport failures cause a retry: timeout, network `TypeError`,
HTTP 5xx and HTTP 429.
- These failures stop at once, with no retry: SHA-256 pin mismatch, the
128 MiB size bound, HTTP 4xx, a missing body, and cancellation. The
SHA-256 pin still checks every byte.
- The body of each rejected (non-2xx) response is canceled before the
next attempt, so a 5xx/429 reply does not hold its connection.
- Each failed attempt writes a warning to the build log, so a later
successful retry does not hide the failure. When all attempts fail, the
error is `Pinned Pi Node download failed after 3 attempts: <cause>`.
- `materialize-pi-distribution.test.mjs`: add tests for stalled headers
and a stalled body that end only when the per-attempt deadline signal
aborts, release of rejected responses before the next attempt, exhausted
attempts, HTTP 503, and the fail-closed cases.
- `package.json`: add `scripts/materialize-pi-distribution.test.mjs` to
the `test:typescript:prep` node:test list. Before this change, no CI
lane ran this test file.

## Verification

- `node --test
packages/paperclip-runner/scripts/materialize-pi-distribution.test.mjs`
→ 13 pass, 0 fail.
- Red checks: with the old materializer, the new tests fail. When the
body cancel is removed, the release test fails. When the deadline signal
is removed from the fetch call, the deadline tests fail.
- `cd packages/paperclip-runner && node
../../scripts/ci-profile/node-test.mjs
scripts/candidate-provider-pack.test.mjs
scripts/materialize-pi-distribution.test.mjs` → 20 pass, 0 fail.
- `node --check` passes on the changed script.
- I did not run the full `pnpm -r typecheck`, `pnpm test:run`, or `pnpm
build` locally, because the sandbox has no installed workspace
dependencies. The changed files are `.mjs` scripts and are not in the
runner `tsconfig.json`. CI runs the full checks.
- A reviewer can read the `Build and push by digest` log of the next
master Docker run. A transient stall now shows `Pinned Pi Node download
attempt 1/3 failed: ...; retrying`.

## Risks

- Low risk. The change affects only the Pi Node archive download. Two
callers use it: the Docker provider-pack build
(`candidate-provider-pack.mjs`) and `paperclipai runtime setup pi`
(`provision-pi.mjs`).
- The worst-case time before failure increases from 60 s to about 195 s
(3 × 60 s + 15 s of backoff). The Docker job has more time than this.
`provision-pi.mjs` has a 900 s setup deadline that it enforces through
`checkCancelled`. The download calls `checkCancelled` before each
attempt, so the deadline and SIGINT/SIGTERM still stop setup. A cancel
can wait for one backoff of at most 10 s.
- For `runtime setup pi`, a stall now prints one warning line on stderr
for each failed attempt. Normal operation prints nothing new.
- The retry does not weaken integrity. The same SHA-256 pin, size bound,
and `redirect: "error"` apply to each attempt. A mismatch does not cause
a retry.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code, with
tool use (shell, file edit, GitHub connector). It read the failed CI
logs, wrote the change and tests, and ran the tests locally.

## 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>
2026-10-28 22:31:36 -07:00
..