Repository navigation
fix(PG-940): the retro table reads 25 PRs a page and retries a failed one - #959
Conversation
ReviewBot · round 1: 4/5 at 836e1f7 · 0 blocking, 2 should-fix, 4 nitsThe retry, the 25-PR page, the cursor in the error and Spec (the right thing)
No spec finding. Standards (built well)
Also reasoned and left to you: Checked patchPath on this machine (sent to the worker):
Its checks, applied in the review clone:
ThreadsNone before this round. This round opens six inline threads for the five findings that sit on a diff line. The first should-fix takes two, one on Checked and Not verified
ReviewBot · round 1. Push back on any finding you think is wrong. |
ReviewBot · round 2: 4/5 at 8a605a9 · 0 blocking, 1 should-fix, 3 nitsEvery round 1 finding is fixed at fb00c03, and the optimizer.md patch went in word for word (the same blob, cee3aca5). The canary merge at 8a605a9 is clean. One problem is left, and round 1's fix direction opened the path to it: a page whose errors come with data keeps that data, but GraphQL nulls the item that failed, and the table code cannot handle a null item. Spec (the right thing)
No spec finding. A fresh two-axis read of the whole diff found no spec gap. Standards (built well)
Reasoned and left as you chose: the inclusive boundary second between chunks, which your body states. Checked patch (the last nit)Path on this machine (sent to the worker):
Checks with the patch applied in the clone:
ThreadsAll six round 1 threads are fixed at fb00c03. Each gets a reply and is resolved this round. This round opens 3 inline threads, for the should-fix and the first two nits. The patch nit is in this comment alone. Checked and Not verified
ReviewBot · round 2. Push back on any finding you think is wrong. |
ReviewBot · round 3: 5/5 at 3006e0a · 0 blocking, 0 should-fix, 1 nitsRound 2's should-fix and nits are fixed at c279c1a and 3006e0a, and the wording patch went in as written (the same blob, b86f07bf). The canary merge af575b7 is clean. A fresh two-axis read of the whole diff found no blocking or should-fix finding. Spec (the right thing)
No spec finding. Standards (built well)
Set aside from the fresh read, each with its reason:
ThreadsAll three round 2 threads are fixed at c279c1a. Each gets a reply and is resolved this round. None stays open, and this round opens none: the nit stays in this comment. Checked and Not verified
The fresh full read found nothing blocking or should-fix, and its one nit is listed above for you to take or leave. ReviewBot · round 3. Push back on any finding you think is wrong. |
|
Merged by bun run pr:merge at 3006e0a.
|
…buntu-26.04 (#1076) ## TL;DR - 🧪 The run-slot lock tests stop failing now and then on GitHub's Linux runner. One such failure held up a ship. - 🔍 The cause was the runner's own `mkdir` command, which can tell two programs that each made the same folder. The tests now use the runner's other `mkdir`, which tells only one, as the Mac's does. ## Summary ```diff tests/scripts/ops/run-slot-fake.ts, machine() slowChild("python3", bin) + if (GNU_MKDIR) // Bun.which("gnumkdir"): ubuntu-26.04 only + bin/mkdir = exec gnumkdir "$@" ``` - GitHub's `ubuntu-latest` is ubuntu-26.04, whose `/usr/bin/mkdir` is uutils coreutils 0.10.0 (`rust-coreutils` 0.10.0-1ubuntu2~26.04.1). Its `create_dir` checks `path.exists()`, then maps a failed `mkdir(2)` to success with `Err(_) if path.is_dir() => Ok(())`, even without `-p`. So when two processes race, both exit 0. - run-slot.sh takes every lock with a plain `mkdir`: the gauges refresh lock, the run slots and the model slots. On that runner, two looks can each hold the gauges lock and both run `top`: "eight looks with no reading yet run top once" got 6. - The image keeps GNU's mkdir as `gnumkdir`, and each fake machine's bin now runs that one wherever it exists. On the Mac and on GNU images nothing changes. - `.agents/testing.md`, Traps: one new trap, the racing `mkdir` and how a test avoids it. **Decisions:** - **The fix is in the test, not in run-slot.sh.** run-slot.sh runs for real only on the Mac, whose BSD `mkdir` is atomic: 60 rounds of the eight-look case there each ran one `top`. Only CI runs the script on Linux. - **The dead-lock takeover case stays out of this PR.** It is a separate race, not yet reproduced alone (Evidence, #959's row), now #1077, so this PR keeps to the two "run top once" cases #956 names. - **The reported lead is refuted.** The lead was that a reading stamped with the look's start time goes stale after a refresh slower than 10 s. The red run took 1.6 s, and six one-second tops in 1.6 s means they ran side by side. A stale stamp cannot do that, while two lock holders can. - **Nothing is posted upstream.** uutils fixed this in 0.11.0 (uutils/coreutils#13970, uutils/coreutils#13986), and Ubuntu's request to bring that fix to 26.04 (LP #2171333) already has a patch waiting for a sponsor. A comment would add no new facts. Once 26.04 takes that patch, the shim is harmless and can go. ## Evidence - **Cause, in an `ubuntu:26.04` container:** 8 racing `mkdir L`, 200 rounds each: | mkdir | rounds with more than one winner | | --- | --- | | `/usr/bin/mkdir` (uutils 0.10.0) | 33 of 200, and 38 of 200 on a second run | | `gnumkdir` (GNU 9.7) | 0 of 200 | - **No repro without uutils:** the eight-look case ran exactly one `top` in 60 of 60 rounds on the Mac (BSD mkdir) and 80 of 80 on `oven/bun:debian` (GNU mkdir, bash 5.2). - **Before / after in `ubuntu:26.04`** (`--cpus 2`, Linux bun 1.4.4): | run | canary's fake | this head's fake | | --- | --- | --- | | probe: 60 rounds of 8 looks with no reading, rounds that ran `top` more than once | 10 of 60 (2 or 3 tops) | 0 of 60 | | `run-slot-gauges-lock.test.ts`, 20 runs, the two "run top once" cases | failed in 6 of 20: the stale-reading case (line 46) in runs 1, 14 and 15, the no-reading case (line 52) in runs 5, 7 and 13 | failed in 0 of 20 | - **At this head on the Mac:** 20 runs in a row of `bun run test:file tests/scripts/ops/run-slot-gauges-lock.test.ts`: 20 of 20 passed, each 10 pass, 0 fail. `bun run check-types:tests`: no errors. `bun run test:skills`: all 13 passed. - **Sightings on CI:** - canary at d216f23, Suite (8): "eight looks with no reading yet run top once", expected 1, received 6, in 1631 ms. - #1063 at 1af2e05, Suite (7) (job 127580442051), ubuntu-26.04: "eight looks at a stale reading run top once", expected 1, received 2, in 1526 ms. The same case fails 3 of 20 on canary's fake above and 0 of 20 at this head. - #959 at 3006e0a, attempt 1, Suite (5) (job 127180040487): line 124 of the same file, "eight looks at a dead lock together each read the stale or the new reading", expected true, received false, in 1627 ms. **Not this race, and not fixed here.** That case still failed 1 of 20 runs at this head in `ubuntu:26.04` (run 12, line 125), with GNU's `mkdir` in the fake. A probe of the case alone found 0 bad rounds in 60 with GNU's `mkdir` and 0 in 60 with all 104 `gnu*` tools on PATH, so no tool swap is shown to fix it. It is #1077. - **doc-sync:** the change touches the test fake alone. `git grep -n -iE "mkdir"` over `.agents`, `scripts/README.md`, `scripts/ops/README.md`, `scripts/hooks/README.md`, `docs` and the core skills finds no line that calls `mkdir` atomic. The new trap in `.agents/testing.md` is the one place that says it is not on ubuntu-26.04. ## Merge Danger **Door:** two-way A revert brings back the uutils `mkdir` in the fake, and the flake with it. **Blast Radius:** small Tests only: every `run-slot*.test.ts` machine on a runner that has `gnumkdir`. run-slot.sh and every other script are unchanged. ## Links - Closes #956 - uutils/coreutils#13970 (the report) and uutils/coreutils#13986 (the fix, in 0.11.0) - #1077: the dead-lock takeover race, left out of this PR - Ubuntu: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2171333 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ding (#1089) ## TL;DR - 🎛️ When run-slot counts how many runs the machine can take, it now uses one CPU reading for the whole count. Before, a new reading that arrived in the middle could give a count that matched neither the old reading nor the new one. - 🧪 This fixes a test that failed now and then on GitHub's Linux runner. When that test fails now, it shows what each check printed. ## Summary ```diff capacity() # slots, room and every take read it + local LANES=unread for s in run-slot run-slot-2 run-slot-3 run-slot-4 slot_open "$s" - ! lanes_open # slots 2, 3 and 4: each read the gauges again + ! lanes_now # the first check reads them, the rest share its verdict +lanes_now() + open|shut: the count's verdict; unread: lanes_open once and keep it; unset: lanes_open, as before ``` - `run-slot.sh slots` prints `capacity() / 2`. Each of slots 2 to 4 called `lanes_open`, and each `lanes_open` read `gauges()` again. In the dead-lock test the stale reading says 30% idle (shut) and the new one 80% (open). A look whose checks straddled the new reading counted slot 2 shut and slots 3 and 4 open, and printed `3`, or `2` when it landed between slots 3 and 4. - The fix is a `local` in `capacity()`, which bash's dynamic scope hands to `slot_open`'s checks. Outside a count, `LANES` is unset and every check runs `lanes_open` as before, so a waiter still reads the gauges on every poll. - `scripts/ops/README.md`, beside the takeover line: one line on the single verdict. **Decisions:** - **The fix is in run-slot.sh, not the test.** A real look on the Mac can count a mixed reading too: the bug isn't specific to Linux, only easier to hit on a 2-CPU runner. - **The take loop is unchanged.** It calls `slot_open` slot by slot to take the first open one, so a reading that lands in the middle only changes which open slot it takes. Its counts come from `capacity()`, which now reads once. - **The dead-lock test now fails on the list of bad looks, not on a boolean.** `expect(looks.filter(neither 1 nor 4)).toEqual([])` prints each bad look's slot line and stderr, so a CI failure says what a look printed. It stays whatever the cause turns out to be. ## Evidence - **The new test**, "a look counts slots 2 to 4 on one reading, even when a new one lands between their checks". It sets a stale 30% reading, a lock a live refresher holds, and a fake `stat`. `stat` runs on the lock only in `gauges_lock`, once its mkdir finds the lock held, so the fake writes a fresh 80% reading after the look's first lanes check has read the stale one, as a refresher's rename would. | | canary's `run-slot.sh` | this head | | --- | --- | --- | | Mac | 10 pass, 1 fail: expected `["1"]`, received `["3"]` | passes | | `ubuntu:26.04` container | 10 pass, 1 fail: received `["3"]` | passes | - **20 runs in a row** of `bun run test:file tests/scripts/ops/run-slot-gauges-lock.test.ts` on the Mac: 20 of 20, each 11 pass, 0 fail. Of the same file in `ubuntu:26.04` (`--cpus 2 --memory=1g`, Linux bun 1.4.4): 20 of 20, each 11 pass, 0 fail. - **Every run-slot test** (`tests/scripts/ops/run-slot*.test.ts`) on the Mac: 424 pass, 0 fail. `bun run check-types:tests`: no errors. `bun run test:skills`: all 13 passed. - **The first look, which caught nothing:** - the dead-lock case alone in `ubuntu:26.04` went 0 bad rounds in 60 with GNU's mkdir, and 0 in 60 with all 104 `gnu*` tools on PATH; - 60 runs of the whole file with a `bash -x` trace per look went 60 of 60 green. So it isn't a uutils tool, and a run can't be relied on to catch it. The deterministic test above is the evidence. - **Sightings on CI:** #959 at 3006e0a, Suite (5), job 127180040487, ubuntu-26.04: the dead-lock case's `slots.every(1 or 4)` received false. In `ubuntu:26.04` at #1076's head it failed 1 of 20 runs. - **doc-sync:** - `git grep -n -E "lanes_open|capacity\(\)|run-slot.sh slots|one reading"` over `.agents`, `docs`, the READMEs, `AGENTS.md` and the skills finds `scripts/ops/README.md:22, 26, 41`; - lines 22 and 41 name `lanes_open` for what it opens, which is unchanged; - the new line sits after line 28's takeover line. ## Merge Danger **Door:** two-way A revert brings back the per-slot readings and the flake. **Blast Radius:** small `run-slot.sh`'s slot count only. A count reads the gauges once instead of up to three times, and the verdict is the same whenever no reading lands in the middle. Every caller outside `capacity()` is unchanged. ## Links - Closes #1077 - #956 and #1076: the mkdir race in the same file, fixed there - #959: the CI sighting 🤖 Generated with [Claude Code](https://claude.com/claude-code)
TL;DR
The retro's table of pull requests no longer loses its rows when GitHub answers one page badly: it asks for smaller pages and asks a failed page again, up to three tries, before it gives up.
When a page still fails, the error names where it stopped, and a retro can now read a wide window in smaller time chunks.
Summary
Evidence
tests/packages/scripts/src/retro/table.test.tswith a fakeghonPATH, one numbered answer per call:Y3Vyc29yOjI1and "3 times", and the third call askedafter=Y3Vyc29yOjI1;QUERYasksfirst: 25;searchOfwith an until readsupdated:A..B.errors(gh's real behaviour, measured in review): exit 0 and its row from one read, the errors on stderr; andpageOfon that answer returns the data and prints the errors;errorscome withdata: null(a rate limit): three reads, then exit 1 naming the first page, with nothing on stdout;--untilwith no time after it: exit 2 with the usage line, and noghcall;(TRIES - 1) * RETRY_MS + 20_000.table.tsswapped in): 3 fail: the 502-once test, the three-failures test, and theQUERYtest onfirst: 25.scripts/briefs/optimizer.md's step 4 describes (grep of the incoming diff for the retro, ops-tick, the roster,session-stateand--log-failed).SKIP_ENV_VALIDATION=true: 7466 pass, 2 fail, both timing-bound under load and outside this diff (tests/api/hono/src/lib/onboard/fix.test.ts's docker-never-starts case readrunningat its deadline;tests/packages/scripts/src/verify/prebuild.test.ts's main-checkout case hit its 30 s timeout), each green in the run at 8a605a9 (116 files, 7454 pass, 0 fail); run again on their own at 3006e0a, the two files read 50 pass, 0 fail. That flag is needed because a fresh worktree's.envfrom.env.exampleleavesPOSTGRES_URLempty and so turns off the sandbox's own skip (packages/scripts/src/test-sandbox.ts:35); without it, 8 api files that import none of this PR's code throw on import. Alsobun run check-types:testsexit 0 (it failed once on apackages/*/distolder than the canary merge, green after the rebuild);bun run test:skillsall 13 passed;docs:wrap --checkexit 0;scripts/check-em-dashes.shno em-dashes;precommit:lightall passed; the pre-commit hooks green.pageOfreads gh's body whatever its exit code, since gh exits 1 on an answer whose errors sit beside its data; the errors test answers with exit 1 and gh's stderr line.--untiltakes the usage exit; the waiting cases' timeouts followTRIESandRETRY_MS;fakeGhmakes itsghrunnable and returns its folder alone;test:skillsbelow.--untiland the next chunk's--sinceshare a second, andA..Bis inclusive at both ends, so a PR updated in that exact second lists in both chunks' tables; the retro reads a row twice at worst, so the brief keeps the plain hand-off.--sinceand--until).Page.nodesandPr.comments.nodesallownull, so the type check makes each reader handle it;readAllskips a null PR androwsOfa null comment.pageOfparses any non-empty body, so an HTML body keeps "a body that does not parse";Answer.exitCode, which nothing read, is gone; the reviewer's wording patch, so the header comment and step 4 copy neitherTRIESnor the page size.scripts/ops/optimizer/auto_end.ts), with no line about the retro table or step 4.tests/scripts/ops/run-slot-gauges-lock.test.ts:124, outside this diff: PG-956, where the CI runner'smkdir(uutils 0.10.0) is not atomic, so two looks can both take the lock. The failed jobs were run again (gh run rerun 42008110585 --failed).scripts/briefs/optimizer.md: step 4 now says that when the table exits 1 naming a page's cursor (a page that failed all its tries), the Optimizer reads the same window again in chunks, each run adding--until <iso>and the next starting its--sincethere, until every chunk exits 0; the reviewer's checked patch, word for word, so the page size and retry counts live in the code alone; writing-for-agents read.git grep -n "retro/table":scripts/briefs/optimizer.md:10(updated) andtests/scripts/briefs/optimizer.test.ts:17, which checks the path alone; no other doc describes the fetch.first: 100over the docs and skills: one hit,extensions/core/skills/playbook/GITHUB-BULK-READS.md:76, a PR'sfilespage in another query, so not this fetch. The docs that name the retro with a table (GLOSSARY.md,docs/playground/playbook.md,extensions/core/skills/playbook/LIFECYCLE.md,web/next/content/docs/cli.mdx) say nothing about its pages or retries.Merge Danger
Door: two-way
A revert brings back the 100-a-page read; no data or state changes.
Blast Radius: retros
Only the Optimizer's retro runs this script; a page that still fails three times exits 1 as before, about 10 s later.
Links
Closes #940
🤖 Generated with Claude Code