Keep the provider-neutral retry policy in the runtime prerequisite while preserving the reviewed Boat stale-lock recovery and database-backed admission proofs.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* commit '0c4bb6fa57bb7c5c3090291617e5b95d93d77a92':
Define the computer admission wait retry contract
Keep resource waits separate from failure accounting and require recorded task-lock and non-assignee authority. Project the provider-neutral contract from the Boat core prerequisite; keep database-backed admission proof in the integration.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Keep exact task claims through preparation cleanup and controller expiry so a crashed computer wait can schedule its single successor. Retain the full before-provider proof and leave terminal tasks, Stop receipts, and contradictory evidence eligible for ordinary cleanup.
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Leave persistent computer cleanup with its active executor and recover inactive release disposition from the exact frozen execution binding and lifecycle policy. Keep sandbox copy-back handoff and conservative invalid-state cleanup unchanged.
Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Close the exact run for stop-and-retain and default stopping paths, retain failed explicit cleanup fences, and settle Stop requests arriving during or after warm retention. Prevent stale warm returns from overwriting a released or rebound lease.
Co-authored-by: Codex <noreply@openai.com>
Bind the new layout policy into provider-pack identity and verify it on controller and guest while preserving legacy immutable packs and approved image root aliases.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Use the same execution-policy guard as heartbeat before exposing Boat
connection, presence or preview actions. Keep exact-owner disconnect
available after the policy changes.
Cover agent and instance defaults with present or absent managed Kubernetes
configuration, including a historical Boat lease and cleanup access.
Co-authored-by: Codex <noreply@openai.com>
Keep requested branch attachment separate from ownership established by realization, and round-trip the optional intent through the shared request contract.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Keep candidate config, credential bindings, computer attachment and activation in one transaction so a rejected key preserves committed authority. Honor configured worktree bases and attach exact existing branches without creating, resetting or replacing dirty work.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Keep requested branch attachment separate from ownership established by realization, and round-trip the optional intent through the shared request contract.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Give the shared target identity regressions a typed computer target and validate retention and retirement when integrated with Boat lifecycle handling.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Assemble the shared runtime prerequisite from the frozen Boat implementation, including its owning tests and the minimal computer target identity dependency.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Integrate e733bddc45 while preserving Boat admission deferral and ownership fences. Verify agent and instance Boat selection without acquiring a lease.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Chat stores pending provider actions in durable database rows.
> - Three action queues still scan every second when empty.
> - These scans keep idle databases active.
> - This pull request wakes those queues after a commit or at their next
deadline.
> - Empty queues stop querying while existing recovery rules remain in
place.
## Linked Issues or Issue Description
Refs #15808, #15802, #15625. Related sleep work: #15750, #15757.
**What existing behavior does this improve?**
Provider messages and notices, accepted GitHub webhook receipts, and
Slack session status synchronization.
**Current behavior**
Chat maintenance scans these three queues every second. It also rechecks
paused Slack sync rows every 30 seconds.
**Proposed behavior**
Reuse the transaction-aware action workers and existing chat_actions
rows. Wake after commits. Restore unfinished work on startup. Use stored
retry and stale-claim deadlines. Stop scanning when empty. Park paused
Slack sync rows until an endpoint change.
## What Changed
- Add three topics to the existing action worker registration. No
tables, migrations, triggers, or external services.
- Signal producers, claims, retry updates, terminal settlements, and
operator resolution inside their owning transaction. Provider I/O
remains outside transactions.
- Share pending-state and deadline predicates between dispatch and
scheduling. Exclude terminal history and uncertain provider sends.
- Preserve GitHub receipt attempt fences and provider-send quarantine.
Preserve Slack's 30-minute refresh while a session is processing.
- Remove these workers from periodic app maintenance. Update the sleep
inventory and add regression coverage.
## Verification
- Passed: 13 focused tests across scheduler, app wiring, and real
PostgreSQL integration. Covers actual commit wakes with deferred fast
paths disabled, GitHub retries and startup recovery, stale-send
quarantine, Slack publication-to-sync delivery, paused deadlines, and
idle queues.
- Passed: pnpm typecheck.
- Passed: full chat integration and related coordinator suites, **1,274
tests across five files**.
- Passed: pnpm build.
- Greptile: **5/5**, no actionable findings, completed on
`dc284cd275e5f6df624d60b27954421f2bcdbee2`.
- Hosted CI: **54 checks passed**, two intentionally skipped Storybook
checks, no failures or pending checks on
`dc284cd275e5f6df624d60b27954421f2bcdbee2`.
Provider responses are simulated. PostgreSQL, transactions, service
dispatch, and scheduling are real. No live Slack or GitHub App was used.
## Risks
- Writers must emit the existing process-local transaction signal.
Independent database clients and direct SQL still need explicit
integration or startup recovery.
- Accepted GitHub receipts are covered. Finding callbacks GitHub never
delivered still uses the separate provider recovery lane.
- Other chat maintenance and persistent transports still run. This does
not establish whole-instance sleep.
- Existing claim, revision, credential, and authorization checks remain
authoritative during concurrent foreground and scheduled dispatch.
## Model Used
OpenAI Codex (GPT-6), with repository inspection, code execution, and
automated tests.
## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work.
> - Heartbeat execution selects an environment before it prepares a
workspace and starts an agent.
> - Workspace preparation now has its own module, but its selection
policy still occupies the executor.
> - Environment defaults, recovery bindings, chat isolation, and
workspace contention must retain their order.
> - This pull request moves that selection phase behind one typed
function.
> - The executor keeps dispatch gates and lease cleanup. Tests cover the
public path and the new boundary.
## Linked Issues or Issue Description
Refs #15807. This continues the heartbeat extraction series after
workspace preparation.
**Current behavior**
The 5,599-line heartbeat service contains a 301-line environment
selection phase inside run execution.
**Proposed behavior**
Move that phase to `heartbeat/environment-selection.ts`. Keep the same
policy, statement order, errors, and public exports.
**Reason and benefit**
Readers can find selection policy in one module and follow the
executor's phases without reading each policy branch.
## What Changed
- Add `selectHeartbeatEnvironment` with typed run, task, policy,
configuration, and service inputs. Return the values needed by
configuration and workspace preparation.
- Preserve native recovery bindings, environment precedence, managed
sandbox enforcement, Kubernetes provisioning, native chat isolation, and
shared workspace contention.
- Reduce `heartbeat.ts` from 5,599 lines / 234,599 bytes to 5,314 lines
/ 220,784 bytes. The new module is 402 lines / 18,215 bytes.
- Add six public-path characterization cases before moving production
code and six boundary cases. Repair plugin-test teardown to drain active
execution and call the database cleanup helper.
- Assert the selected local environment in the browser fixture's real
worker and document the new boundary.
## Verification
- Before the move: 71 tests passed, including all six new public-path
cases. Three plugin tests skipped when the host could not start their
database probe.
- After the move: all 112 focused tests passed across eight files, with
no skips. These cover environment policy, local and plugin execution,
contention, recovery bindings, workspace preparation, chat isolation,
and low-trust directories.
- Passed `pnpm -r typecheck` and `pnpm build`.
- A normalized TypeScript AST comparison confirms the 301 moved lines
and remaining executor statements are unchanged, apart from the new call
and imports.
- Browser acceptance: the task UI recovered automatically and the worker
verified its selected local environment, preserved uncommitted files,
and completed the task. The persistent-failure case passed in isolation:
three total runs, then a blocked task that remained blocked after
reload.
- The first persistent-failure browser attempt missed the retry banner
within five seconds, despite a persisted scheduled retry. Its unchanged
isolated rerun passed. The trace is retained; the intermittent UI
failure is not yet attributed to a root cause. No assertion or timeout
was relaxed.
- Greptile reviewed `20953dd164decb565bc8d85081aad31a23fd33e5`: 5/5, no
actionable findings.
- Hosted CI passed on `20953dd164decb565bc8d85081aad31a23fd33e5`: 52
successful checks and two intentionally skipped Storybook checks, with
no pending or failing checks. This includes all general and serialized
test groups, Runner verification, eight browser shards, typecheck, and
build.
- The duplicate local `pnpm test:run` was interrupted after hosted CI
completed. It is not reported as a local full-suite pass. Local
completed checks are the 112 focused tests, typecheck, build, and
browser cases described above.
## Risks
- This selection phase enforces execution restrictions. A wrong input or
import could select the wrong environment or change retry behavior.
Public-path tests cover refusal, redirect, contention, and dispatch;
boundary tests cover recovery and isolation.
- The helper can provision a managed environment row and add concurrency
notes to the supplied task context. It acquires no execution lease. The
executor retains its catch/finally boundaries and resource ownership.
- Kubernetes selection is tested with real database state and bootstrap
settings. These checks do not claim execution against a live Kubernetes
cluster or hosted sandbox provider.
- No schema, migration, API contract, or intended behavior change.
## Model Used
OpenAI Codex (GPT-6), with repository inspection, code editing, compiler
checks, database integration tests, and browser automation. The exact
serving model ID and context-window size are not exposed in this
session.
## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
Co-authored-by: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Pull requests to this repository run a large CI matrix. The slowest
check sets the time a contributor waits for a green run.
> - After #15849 hid the verifier's downloads behind package staging,
the `Build` lane is still the slowest check in five of the eight fully
green PR runs created after that merge (535s to 581s; the top server
shard is 529s to 560s in the other three). `Build` is about 100s of
setup, 160s to 176s of `pnpm build`, and 265s to 312s of the public
install verifier.
> - Inside `pnpm build`, every package after `@paperclipai/shared` waits
on the Runner: its `build` script takes 76s to 80s, and for 65s of that
it is the only job running. The chain runs `build:typescript` three
times: once directly, then again inside `check:semantic-contracts` and
`check:runner-workflow-traceability`, which rebuild first so they work
standalone. The server's `prepare:runner-vendor` then runs the whole
Runner build a second time, so the job pays for four redundant
TypeScript passes.
> - Each pass is two `tsc` projects, the eval-kernel rebuild, and the
schema checks, 7s to 8s on the CI runner. The dist they rebuild is the
dist the first pass just wrote; nothing between the passes changes it.
> - This pull request calls the two check scripts directly after the
chain's own `build:typescript`, the way `generate-replay-goldens.mjs
--check` already does in the same line. The standalone `check:*` scripts
keep their rebuild.
> - The benefit is about 30s off the `Build` job's critical path, about
15s off the Typecheck job and every Docker or release build that runs
the server build, with no change to what is checked.
## Linked Issues or Issue Description
**Describe the enhancement**
The `Build` PR check is the slowest check in most green PR runs. Its
`pnpm build` step compiles the Runner's TypeScript surface four times:
three passes inside one Runner `build`, and a second full Runner `build`
from the server's `prepare:runner-vendor`. Only the first pass produces
a dist that the later steps read; the other three rewrite the same
output.
**Why is this needed**
The slowest check decides the wall-clock time of every PR run. In the
eight fully green PR runs created after 2026-10-11 04:10Z, `Build`
topped five of them at 535s to 581s. The Runner build sits alone on the
`pnpm build` critical path for about 65s, and the server build repeats
it. From the job logs of four of those runs, the second and third
`build:typescript` passes cost 7s to 8s each, in both Runner builds, so
about 30s per job.
**Proposed solution**
In the Runner's `build` script, replace `pnpm run
check:semantic-contracts && pnpm run check:runner-workflow-traceability`
with `node scripts/generate-semantic-contracts.mjs --check && node
scripts/check-runner-workflow-traceability.mjs`. Both scripts read
`dist/`, which `build:typescript` wrote earlier in the same chain, and
the steps between them are `--check` runs that write nothing to `dist/`.
Add a guard test so the chain compiles TypeScript exactly once and both
checks still run after it.
**Alternatives considered**
- Skip the server's `prepare:runner-vendor` rebuild when `pnpm -r build`
already built the Runner. That rebuild is what makes `pnpm --filter
@paperclipai/server build` correct standalone (the Dockerfile uses it),
and the server's build-script test pins the command. A freshness check
would be a larger semantic change; this PR shrinks the rebuild instead
of removing it, and the next cycle can measure what remains.
- Make `tsc` incremental across the passes. It would still re-run the
schema checks and the eval-kernel build each time, and would add
build-info files to the package.
- Change `check:semantic-contracts` and
`check:runner-workflow-traceability` themselves to stop rebuilding. They
are documented standalone commands (`pnpm --dir
packages/paperclip-runner check:semantic-contracts` in the capability
docs) and must work from a stale dist; unchanged here.
## What Changed
- `packages/paperclip-runner/package.json`: the `build` script runs
`node scripts/generate-semantic-contracts.mjs --check` and `node
scripts/check-runner-workflow-traceability.mjs` directly, after its own
`build:typescript`, instead of the `check:semantic-contracts` and
`check:runner-workflow-traceability` wrappers that rebuild first. Every
other step, its order, and the standalone `check:*` scripts are
unchanged.
- `server/src/__tests__/server-package-build-script.test.ts`: a new test
in the suite that already pins the server's Runner vendoring reads the
Runner's `build` script, resolves every `pnpm run` step, and asserts
that exactly one step compiles TypeScript (directly or through a
wrapper), that both check scripts are in the chain, and that both come
after `build:typescript`. It fails against the previous script with
`expected [ 'build:typescript', …(2) ] to deeply equal [
'build:typescript' ]`.
## Verification
- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/server-package-build-script.test.ts`: 9 pass (1 new). With
the `package.json` change stashed, the new test fails as described above
and the other 8 pass.
- In `packages/paperclip-runner`, the new chain without the cargo step
(`check:protocol-manifest`, `build:typescript`,
`check:capability-contract`, `check:capability-inventory`,
`check:protocol-coverage`, `generate-semantic-contracts.mjs --check`,
`check-runner-workflow-traceability.mjs`, `generate-replay-goldens.mjs
--check`, `generate-semantic-action-catalog.mjs --check`) exits 0:
`Runner stress traceability passed: 44 findings, 12 workflows, 1
explicit exclusion.` and `Replay golden snapshots and parity summaries
are current.` Cargo is not installed on the host that produced this PR,
so `build:binary` was not run locally; it is unchanged and runs in this
PR's `Build` job.
- `pnpm run build:typescript` alone takes 5.8s to 5.9s on this host, so
each removed pass is in the range the CI logs show.
- This PR's own `Build` job runs `pnpm build` from the branch checkout,
so its log shows the new chain in both Runner builds. Expected on that
job: `pnpm build` about 130s to 146s (was 160s to 176s on the four
measured runs), `Build` about 505s to 551s (was 535s to 581s).
**Measured on the four post-#15849 `master` runs that motivated this
PR** (`Build` job logs):
| Run | `pnpm build` | Runner build in `pnpm -r build` | Extra
`build:typescript` passes in it | Runner rebuild in server build | Extra
passes in it |
| --- | --- | --- | --- | --- | --- |
| 38112764439 | 169s | 76s | 04:51:52 and 04:52:00 (about 14s) |
04:53:12 to 04:53:41 | 04:53:26 and 04:53:34 (about 15s) |
| 38111191543 | 176s | 79s | 04:22:40 and 04:22:48 (about 14s) |
04:24:04 to 04:24:34 | 04:24:18 and 04:24:26 (about 15s) |
| 38110914319 | 160s | 75s | 04:19:07 and 04:19:14 (about 14s) |
04:20:25 to 04:20:53 | 04:20:39 and 04:20:46 (about 14s) |
| 38110897745 | 172s | 80s | 04:15:16 and 04:15:25 (about 15s) |
04:16:41 to 04:17:11 | 04:16:55 and 04:17:03 (about 15s) |
## Risks
- Low risk. The same two checks run against the same dist, in the same
position in the chain. The only difference is that the dist is not
rewritten twice between them.
- If a future step between `build:typescript` and the two checks starts
writing to `dist/`, the checks would read that output instead of a fresh
rebuild. The steps there today (`check:capability-contract`,
`check:capability-inventory`, `check:protocol-coverage`) are `--check`
runs that only compare generated spec files.
- The standalone `check:semantic-contracts` and
`check:runner-workflow-traceability` scripts still rebuild first, so
documented usage is unchanged.
- The guard test lives in the server's suite because that suite already
pins the Runner vendoring contract and runs in the server shards on
every PR.
## Model Used
- Claude (Anthropic), model ID `claude-fable-5-1`, running as a
Paperclip agent through the Claude local adapter with tool use (shell,
file edits, GitHub CLI). The agent collected the CI data, made the
change, ran the tests, and wrote this description.
## 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)
- [ ] 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Treat completed question-response deliveries as history only after their exact same-company handoff finishes. Preserve missing evidence, retries, accounting debt, and idle-sleep fences.
Add a concurrent all-state question history index with interrupted-build repair. Keep migrations serialized without waiting snapshots that deadlock concurrent index builds.
Validated by 118 focused sleep tests, six migration/snapshot tests, full repository typecheck, all CI gates, and Apex 5/5.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - Pull requests to this repository run a large CI matrix. The slowest
check sets the time a contributor waits for a green run.
> - After #15826 leveled the server shards and #15831 split
`workspaces-b`, the `Build` lane is the slowest check: 634s in run
38107578322 (2026-10-11 03:11Z), ahead of the next server shard at 574s.
It was 533s in run 38107430861 and 543s to 568s in earlier runs that
night.
> - `Build` is about 100s of setup, 148s to 195s of `pnpm build`, and
278s to 320s of `scripts/verify-grok-npm-install.mjs`. The verifier
stages and packs the public packages first (84s to 100s of CPU work),
and only then pulls the pinned `node:24-trixie` consumer image (8
layers, 440 MB compressed) and downloads the Grok binary (65 MB gzip,
166 MB verified). Both network waits sit after the staging work on the
critical path.
> - Neither download depends on the packed packages. The image is pinned
by digest and the Grok binary is checked against its pinned sha256 by
the provisioning script, so starting them early changes no verified
property.
> - This pull request starts both downloads as background children
before staging and joins them where they are needed: the image before
the first sandbox, the binary after the negative probe. It also prints
one timing line per phase so the next slow part is visible in the CI
log.
> - The benefit is that the whole pull and the whole Grok download hide
behind the staging phase. The verifier should shed the full pull time on
a fresh runner, and the next cycle can read the phase split instead of
inferring it from log gaps.
## Linked Issues or Issue Description
**Describe the enhancement**
The `Build` PR check is now the slowest check in the run. Its last step,
the public npm install verifier, pulls the consumer Docker image and
downloads the Grok binary only after it has finished staging and packing
the eighteen public packages. Both downloads could run while the
packages are staged.
**Why is this needed**
The slowest check decides the wall-clock time of every PR run. In the
most recent fully green run (38107578322), `Build` took 634s: `pnpm
build` 195s and the verifier 320s. The verifier's log shows the staging
phase ends at +100s and the Grok result arrives at +267s. The image pull
and the Grok download are inside that second window, in series with the
consumer install and the probes.
**Proposed solution**
Start `docker pull --platform linux/amd64 <pinned image>` and
`provision-grok.mjs` as background children at the top of the verifier,
before the staging loop. Await the image right before the first sandbox
call. Provision Grok into a prefetch directory and rename it into
`native/grok` only after the "npm must not provision Grok" assertion and
the negative probe, so that assertion keeps its meaning and only the
positive probe sees the binary. Print a
`paperclip.public-npm-install.timing.v1` line with wall time per phase
and the time the main flow waited on each prefetch.
**Alternatives considered**
- Run the verifier as its own job in parallel with `Build`. It needs the
full `pnpm build` output, so that job would be as long as `Build` is
now.
- Warm the consumer npm cache from an Actions cache. That changes the
"clean public npm install" property the verifier reports. Not done here;
the new timing line will show whether the consumer install is worth that
discussion.
- Pack the eighteen packages concurrently. The three bundled packages
run their own npm installs in series inside `prepareBundledPackage`,
which is synchronous. A larger restructure; left for a later cycle with
timing data.
- Overlap the Pi runtime setup with the Grok probes. Both use the same
consumer directory from separate containers, and the probes after
provisioning are short. Not worth the concurrency on shared state.
## What Changed
- `scripts/prefetch-child.mjs` (new): `startPrefetch({ name, cmd, args,
cwd, env, logPath })` spawns a background child with no inherited
terminal and its stdout and stderr in a log file, and returns `{ name,
child, done, durationMs }`. `done` resolves on exit 0 and otherwise
rejects with the tail of the log; the rejection is observed inside the
helper, so an unawaited failure never becomes an unhandled rejection.
`durationMs` ends at the child's last write to its log (the exit event
is observed late when the caller is blocked in synchronous work; both
downloads print their final status line as they complete).
`stopPrefetches(prefetches, { graceMs })` sends `SIGTERM` to every child
still running, escalates to `SIGKILL` after the grace period, and
resolves only once all of them have exited.
- `scripts/verify-grok-npm-install.mjs`: the image pull and the Grok
provisioning start through the helper inside the `try` block, before the
staging loop. The main flow awaits the image before the first `npm
install` sandbox and awaits Grok after the negative probe, then renames
the staged binary into `native/grok` before the positive probe. The
`finally` block awaits `stopPrefetches` and only then removes the
scratch root, so cleanup never races a child's writes. The script prints
one timing JSON line after the Pi probe, with the per-phase wall times,
the two `*_wait_ms` values, and the corrected `*_prefetch_ms` values.
- `scripts/__tests__/prefetch-child.test.mjs` (new): seven tests run
small local Node children, with no Docker or network: a successful
prefetch resolves and keeps its output; the duration ends at the
download even when the parent is blocked for 1.5s; a nonzero exit
rejects with the log tail; a command that cannot start rejects instead
of hanging the join or the stop; stopping a running child waits for its
exit; a child that ignores `SIGTERM` is killed after the grace period;
an unawaited failure does not produce an unhandled rejection in a fresh
process.
- `scripts/__tests__/grok-public-install-sandbox.test.mjs`: the source
test asserts that exactly the pinned image is pulled for `linux/amd64`,
that Grok is provisioned into the prefetch directory and never straight
into `native/grok`, that both prefetches start before staging, that the
image is joined before the first sandbox, that the rename happens after
the npm assertion and the negative probe and before the positive probe,
that the verifier imports the helper, and that `stopPrefetches` is
awaited before the single `rmSync(root, ...)`.
- `package.json`: `test:release-registry` runs the new test file.
## Verification
- `node --test scripts/__tests__/prefetch-child.test.mjs
scripts/__tests__/grok-public-install-sandbox.test.mjs
scripts/__tests__/cursor-public-install-sandbox.test.mjs
scripts/__tests__/retain-runner-qualification-packages.test.mjs`: 22
pass (7 new in `prefetch-child.test.mjs`, 12 in the sandbox file). The
new file passed on three consecutive runs.
- `node --check scripts/verify-grok-npm-install.mjs` passes.
- `node packages/paperclip-runner/scripts/provision-grok.mjs
<scratch>/prefetch/grok` on this host: `Verified Grok 1.0.13
(linux-x64)` in 1.0s, `native/grok` absent before the move, 166079904
bytes in place after it.
- Docker is not installed on the host that produced this PR, so the
verifier was not run end to end locally. This PR's own `Build` job runs
`node scripts/verify-grok-npm-install.mjs` from the branch checkout, so
it exercises the change in full.
**Measured on this PR's first CI run**
([38108964834](https://github.com/paperclipai/paperclip/actions/runs/38108964834),
created 03:37Z), against two `master` runs created in the same twelve
minutes:
| | 38108878166 (`master`, 03:35Z) | 38108995800 (`master`, 03:37Z) |
38108964834 (this PR, 03:37Z) |
| --- | --- | --- | --- |
| Verifier step | 307s | 248s | 219s |
| `Build` job | 555s | 484s | 459s |
| Slowest check in the run | `Build` 555s | server (8/12) 535s | server
(12/12) 547s |
The PR run's timing line: `stage_packages_ms 84464`, `image_wait_ms 1`,
`consumer_install_ms 97305`, `lifecycle_rebuild_ms 3545`,
`codex_probe_ms 1211`, `grok_wait_ms 0`, `grok_probes_ms 626`,
`pi_setup_ms 22194`, `pi_probe_ms 7753`. Both waits are at most 1ms, so
the staging phase hid both downloads in full. The window from the end of
staging to the Grok result went from 161s to 167s on the two runs that
motivated this PR to 102s here, so the hidden downloads were about 60s
on a fresh `runs-on` spot instance. Against the two same-window `master`
runs, the verifier step is 29s to 88s shorter and `Build` is 25s to 96s
shorter; the spread is runner variance in `pnpm build` and the consumer
install.
The `image_prefetch_ms` and `grok_prefetch_ms` values on that first run
(84550 and 186696) were the parent's late observation of each exit, not
download times; that is the first Greptile finding, fixed by the second
commit, which measures from the download's last output instead.
## Risks
- Low risk. The verifier proves the same properties in the same order.
The only ordering change is that the downloads start earlier; every
assertion, probe, and sandbox argument is unchanged.
- The Grok binary is written under the scratch root in a `prefetch`
directory during staging. That directory is never mounted into a
container, and the `native/grok` assertion still runs against an absent
path.
- A prefetch that fails early now fails the step at its join point,
where the old code would have failed at the same download a few seconds
later. Same outcome, same message source.
- `*_prefetch_ms` is a last-output time. A child that printed early and
then kept working silently would be under-reported; neither `docker
pull` nor `provision-grok.mjs` behaves that way (both print their final
status as they finish), and a silent child falls back to the observed
exit time.
- Cleanup now waits up to 5s for a child that ignores `SIGTERM` before
killing it, so a failing run can end up to 5s later than before. Both
children exit promptly on `SIGTERM` in practice.
- The timing line is one more stdout line. Nothing in the repository
parses the verifier's stdout; the two workflows that run it write the
output to the job log or to a file.
## Model Used
- Claude (Anthropic), model ID `claude-fable-5-1`, running as a
Paperclip agent through the Claude local adapter with tool use (shell,
file edits, GitHub CLI). The agent collected the CI data, made the
change, ran the tests, and wrote this description.
## 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)
- [ ] 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: devinfoley <139239+devinfoley@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>