Skip to content

fix: useId mismatches when deferring props. - #258

Merged
cpojer merged 1 commit into
mainfrom
defer-fix-2
Jul 6, 2026
Merged

cpojer merged 1 commit into
mainfrom
defer-fix-2

Conversation

@cpojer

@cpojer cpojer commented Jul 6, 2026

Copy link
Copy Markdown
Member

@cpojer
cpojer requested a review from Brooooooklyn July 6, 2026 09:19
@cpojer
cpojer enabled auto-merge (squash) July 6, 2026 09:20
@cpojer
cpojer requested review from TheAlexLichter and fengmk2 July 6, 2026 11:32
@cpojer
cpojer merged commit 504af69 into main Jul 6, 2026
7 checks passed
@cpojer
cpojer deleted the defer-fix-2 branch July 6, 2026 12:09
cpojer pushed a commit that referenced this pull request Sep 11, 2026
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
cpojer pushed a commit that referenced this pull request Sep 11, 2026
…work (#259)

* fix(api): bound the source download, and make GitHub's Re-run button work

Build bld_33gs31a2x6qj (voidzero.dev) ran 16m52s and failed "Build exited
with code 1" with a single log line. Cloudflare telemetry showed the
container did nothing for the whole run: 2.5s of CPU total, memory flat at
the agent's own 145 MB, disk grown 5.4 MB, ~4.4 MB received of a 20.8 MB
tarball in bursts separated by dead minutes. A successful build of the SAME
repo 12h earlier: 2137 MB memory, 968 MB disk, 1521 KB/s peak. The
BuildManagerLarge DO polled the agent ~60/min for the full 17 minutes, every
alarm `success` — log transport was healthy; the agent was blocked in
downloadSource() with nothing to report.

`downloadSource` fetched the tarball with no timeout, no stall detection and
no retry. undici cannot catch this: `bodyTimeout` resets on every chunk, so
any trickle defeats it.

Bound it on three axes:

- SOURCE_STALL_TIMEOUT_MS (20s with NO bytes) is the guard that does the
  work. A healthy download streams continuously, so it cannot fire on one.
- SOURCE_PHASE_BUDGET_MS (240s) is the only wall clock and spans every
  attempt together, sized inside the smallest plan budget (free = 10min).
- 3 attempts, each from an empty WORK_DIR. 401/403/404/410 fail on the first
  attempt; 408/425/429/5xx, transport errors and a non-zero tar exit retry.

Deliberately NO per-attempt wall clock and NO throughput floor: either would
newly fail a large repo on a slow but honest link.

The download also logs progress every 15s. Before this the agent logged one
line before the fetch and the next only after extraction, so a 17-minute
stall was indistinguishable from a busy build in the live tail AND in the
check-run tail. That opacity is why the incident took telemetry to diagnose.

extractTarball settles ONLY from the child's 'close' (SIGTERM then SIGKILL
after EXTRACT_KILL_GRACE_MS). Settling from the pipeline rejection let the
retry loop rm -rf WORK_DIR under a live tar, and the old child's later
'close' cleared `activeChild` out from under the new attempt — making a
subsequent cancel a silent no-op. `activeChild` is now cleared by identity.
The WORK_DIR reset is async: this agent is PID 1, and a synchronous
recursive delete blocks the stall watchdog, the control server (so /cancel
and the DO's log pull stop answering) and the event loop. The phase deadline
is re-checked after it.

Separately, the check-run failure summary now carries onStop's `reason`
("Build exited with code 1 (exit).") — the one bit that separates an agent
exit from a runtime kill, which the DB row always had and the surface most
people see did not.

Finally, GitHub's "Re-run" / "Re-run all checks" buttons now do something.
There was no `check_run` case at all: the button rendered, the click
delivered an event, and the API answered "unhandled" — so the only way to
retry a failed build was to push a new commit. `rerunBuildForCommit` reuses
the PUSH pipeline (connection resolution, renamed-repo self-heal, the
fail-closed branch-head gate, supersedeAndCreateContainerBuild) so a rerun
can never deploy a commit a push could not.

NOTE: the App must SUBSCRIBE to `check_run` and `check_suite`. That switch is
separate from `Checks: Write`; without it GitHub renders the button,
delivers nothing, and the click stays a no-op. See meta/operations.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EahVSpn3Gx6qsisjxYGhC

* chore: reformat sandbox.test.ts for the current oxfmt

`vp fmt --check` has been red on main since the vite-plus 0.2.9 bump (#256);
#257 and #258 both merged with lint-and-check failing. This is a mechanical
oxfmt-only change, unrelated to the build-agent work in this PR, but CI gates
on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EahVSpn3Gx6qsisjxYGhC

* fix(api): close the adversarial-review findings on the rerun path

Six rounds of review against the source-download bounds and the Re-run
webhook. Each finding was reproduced in the code before being acted on, and
each fix is pinned by a test that fails when the fix is reverted.

Re-run webhook:

- A per-check rerun now targets ONE project. `external_id` (the build id
  `createCheckRun` stamps on every run) resolves the originating build and
  narrows candidates to its project. `project_id` is unique on
  `github_connections` but repo+branch are not, so resolving by branch alone
  rebuilt every sibling project from a single click — and could enqueue a
  container build from a click on another project's `github_actions` check.
- It FAILS CLOSED when that build cannot be resolved. Deleting a project
  cascades its build rows away while GitHub keeps check runs for months, so
  the branch fallback would let an orphaned check rebuild siblings owned by
  other users.
- `head_branch` is no longer a filter. GitHub keeps one suite per app per
  commit even when that commit heads several branches, so filtering on it
  dropped the connection of a project tracking the other branch at the same
  SHA. The per-branch freshness gate decides instead, and it is exact.
- A rerequest that enqueues nothing now COMPLETES the clicked run with a
  reason, because GitHub flips the suite back to `queued` on the click. A
  refusal ECHOES the run's existing conclusion and never writes one of its
  own: GitHub counts `neutral`/`skipped` as PASSING for a required status
  check, so completing a run we did not run with a passing conclusion would
  let anyone with rerun rights clear branch protection. `stale` is observable
  but not writable, so it maps to `cancelled`. Only an authoritatively
  `complete` AND live build writes `success` — that reuse branch returns the
  existing row NON-null, which used to read as an enqueue and poke a
  scheduler that could not claim it. Every other reuse is real work,
  including `queued`.
- Reporting runs on `executionCtx.waitUntil`, off the response path. GitHub
  drops a delivery unanswered for 10s and does not redeliver failures
  automatically, while `completeCheckRun` mints a token and retries with no
  clock of its own.

Source download:

- `extractTarball` settles ONLY from the child's `close`, so a retry cannot
  wipe WORK_DIR under a live extraction. A real `tar` dies too promptly to
  prove that, so the test injects a child that outlives the rejection.
- 403 is classified from headers and a bounded body prefix, not status
  alone: GitHub answers 403 for a permission failure AND for both kinds of
  rate limit. Status and headers classify FIRST — reading the body of a
  response already known to be rate limited could only lose its `retry-after`
  to the stall watchdog. A rate-limited response naming no delay waits the
  documented minimum; `x-ratelimit-reset` counts only when
  `x-ratelimit-remaining` is `0`, since it rides along on every response and
  an hour-away routine reset would turn a retryable 500 fatal.
- Every non-ok response body is cancelled. undici holds the socket for one
  that is neither consumed nor cancelled.
- `WORK_DIR` takes a test-only override. `build-manager.test.ts` wipes the
  same absolute path in a parallel worker, which made the real-tar harness
  flaky.

Deliberately NOT changed, with reasoning recorded in code and README: a
`check_suite` refusal reports nothing (the Checks API cannot conclude a
suite, and both ways to infer its runs are unsafe or unverified), and
`sender` is not authorized against per-project deploy rights (no such
mapping exists — a connection carries one Void user).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EahVSpn3Gx6qsisjxYGhC

* fix(api): compare rerun branches concurrently

A `check_suite` fan-out owes one branch-head compare per DISTINCT branch.
Awaiting them in series multiplied the single compare the push path affords
by the branch count, against GitHub's 10s delivery deadline — which GitHub
enforces by dropping the delivery, with no automatic redelivery, so the
Re-run click is simply lost.

Run them through `Promise.all`. The compares write distinct keys with no
read-modify-write on `freshness`, and `getBranchHeadComparisonStatus`
resolves rather than throws on every failure path, so one bad branch cannot
become a 500. Latency now tracks the slowest branch instead of their sum.

Warm the installation token first: `getInstallationToken` memoizes per
installation, so the serial loop paid one mint and reused it, while a cold
cache under `Promise.all` would stampede N mints. The warm-up ignores its
own failure — each compare still mints and retries on its own.

Codex asked for the whole rerun to move to durable/background processing
instead. Refused: that is an architectural change to webhook handling that
the push path would have to follow, and the per-branch multiplication is the
only part this feature added. `getBranchHeadComparisonStatus` having no
fetch timeout is the true root cause, but push and branch-delete share it,
so that is a separate change.

The new test parks each compare until both are in flight at once. With the
serial loop restored it fails `expected 1 to be 2` after both barrier
deadlines expire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EahVSpn3Gx6qsisjxYGhC

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

defer() causes useId hydration mismatches on every eager component

2 participants