Skip to content

ci(soak): add a dispatchable 20-iteration convergence soak (BLO-28888) - #1419

Merged
allyblockcast[bot] merged 9 commits into
masterfrom
blo-28888-soak-workflow
Aug 22, 2026
Merged

ci(soak): add a dispatchable 20-iteration convergence soak (BLO-28888)#1419
allyblockcast[bot] merged 9 commits into
masterfrom
blo-28888-soak-workflow

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • The heartbeat dispatch subsystem is covered by server/src/__tests__/heartbeat-queued-backlog-convergence.test.ts, which is the acceptance evidence for BLO-20396's queue-convergence criteria
  • BLO-20885 fixed a timing-sensitive case in that file (AC1, merged in test(heartbeat): drop the 3s waitForStarted bounds in the resume-cap case (BLO-20885) #1380) and proved the fix isn't vacuous (negative control, test(heartbeat): make the queued-backlog convergence suite deterministic (BLO-20885) #1011), but its AC2 — "20 consecutive runs, no failure, at ONE head" — is still open
  • AC2 needs an uninterrupted 50–90 minute process, and an agent pod has failed to deliver one four times across three assignees over four days; every one of those deaths was dispatch, not the assignee
  • This pull request moves AC2's venue to a workflow_dispatch CI job, where a 90-minute process is not a constraint — and where AC2's own verifying signal already pointed ("in the General tests (server *) shard")
  • The benefit is that AC2 becomes achievable and repeatable instead of regenerating a long_active_duration productivity review every 6h — five have fired on BLO-20885 already

Linked Issues or Issue Description

This changes the venue, not the bar. Still 20 consecutive green iterations at a single head, not pooled across heads.

What Changed

  • .github/workflows/soak-heartbeat-convergence.yml (new) — workflow_dispatch-only job. Inputs: ref (required — the single head AC2 is measured at), iterations (default 20), load_workers (default auto). runs-on: default, timeout-minutes: 240, uploads the artifact with if-no-files-found: error.
  • .github/scripts/soak-heartbeat-convergence.sh (new) — runs the test file N times, one fresh vitest process per iteration, invoked from server/ so server/vitest.config.ts (pool: forks, maxWorkers: 1, isolate: true) applies. Records rc, duration, load0 and nproc per iteration and emits summary.txt in the same table shape as the two hand-run series. Exits non-zero if any iteration fails.
  • .github/scripts/tests/soak-workflow-triggers.test.mjs (new) + one step in pr.yml's policy job — asserts the soak stays dispatch-only, that no other workflow invokes it, and that it doesn't quietly become a --repeat run.

Two things worth calling out, because both are easy to get subtly wrong:

--repeat 20 is not 20 runs. It repeats each test inside a single process, so it never re-exercises process startup, the global setup, or the embedded-Postgres lifecycle — which is exactly where this file's timing sensitivity lives. The script forks a new process per iteration; the guard test asserts --repeat doesn't appear.

The load caveat is carried forward, not dropped. Series 1's only failure (iter-14) appeared solely once load reached ~11 on 48 cores (~0.23/core) and iteration time roughly doubled from its ~131s baseline. A soak on an idle runner can pass 20/20 without ever reaching that condition — a vacuous green, worse than no evidence. So the script can hold synthetic CPU load (auto = ceil(nproc × 0.229) busy-loop workers, with a 90s warm-up because the 1-minute load average is a decay, not a gauge), and the summary states explicitly whether the regime was reached, naming a green below it as weak evidence rather than asserting more than the data supports.

Verification

Guard test and the repo's own workflow-label validator, locally:

$ node --test .github/scripts/tests/soak-workflow-triggers.test.mjs
✔ soak workflow is dispatch-only
✔ no other workflow invokes the soak
✔ soak runs the script rather than an inline --repeat
✔ soak takes a caller-supplied ref and defaults to 20 iterations
ℹ pass 4  ℹ fail 0

$ node ./scripts/check-github-runner-labels.mjs
Validated 26 workflows: all runner labels use ARC.

The script's mechanics were exercised end-to-end against a stubbed test runner (fast, no 90-minute wait) across green / red / input-validation paths. Green path with load_workers=auto on a 32-core host:

head=<sha> started=2026-08-19T08:54:59Z nproc=32
iterations=3 process_model=one-fresh-vitest-process-per-iteration (not --repeat)
synthetic_load_workers=8 (mode=auto) baseline_load0_before_workers=11.55
iter=01 rc=0 dur=1s load0=12.06 | Test Files 1 passed (1) | Tests 11 passed (11)
...
load0 min=11.66 mean=11.93 max=12.06 | per-core min=0.364 mean=0.373 max=0.377 (nproc=32)
iterations at or above the series-1 failure ratio (0.229 load/core): 3/3
REGIME: REACHED for every iteration.
result=GREEN failures=0/3

Red path (iteration 2 forced to fail) records iter=02 rc=1, ends result=RED failures=1/3, and the script exits 1 — so a failing iteration fails the job rather than being swallowed. ITERATIONS=abc exits 2. That smoke test also caught a real defect before it could waste a CI run: when git rev-parse HEAD failed the header emitted head=HEAD, which would have produced an artifact that couldn't name the head it soaked — the whole point of AC2. It now hard-fails on a non-40-hex SHA.

Full verification is the dispatch itself, which can only run once this is on master (workflow_dispatch reads the workflow from the default branch). Post-merge I'll dispatch at the then-current master head and paste the run URL + per-iteration table on BLO-20885. That run — not this PR — is AC2's evidence.

Risks

Low, and structurally bounded: nothing here runs on any existing trigger. The soak is workflow_dispatch-only, so merging it changes the behaviour of zero current CI paths. The only always-on addition is one node --test step in the policy job (~0.3s, no network).

  • Could it get onto the per-PR path later? That's the failure mode worth guarding, so it's the one thing under test — the guard fails the PR gate if the trigger list ever changes or another workflow references it.
  • Synthetic load overshoot. auto adds ceil(nproc × 0.229) workers on top of whatever the runner already carries, so on an already-busy node it overshoots the target ratio. Overshoot is the safe direction (it exercises contention harder, and the summary reports the actual ratio rather than the intended one), and timeout-minutes: 240 leaves real margin over the 50–90 minute expectation. load_workers=off opts out.
  • Runner cost. ~50–90 minutes of self-hosted time per dispatch, incurred only when a human or agent explicitly asks for it.

Model Used

  • Claude Opus 4.5 (claude-opus-5[1m], 1M context), extended thinking, agentic tool use — running as the Paperclip Release Engineer agent.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — N/A, no UI surface
  • I have updated relevant documentation to reflect my changes — the workflow and script carry their rationale inline; no external doc references this venue yet
  • 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
  • I will address all Greptile and reviewer comments before requesting merge

BLO-20885's AC2 — 20 consecutive green runs of
heartbeat-queued-backlog-convergence.test.ts at ONE head — needs an
uninterrupted 50-90 minute process. An agent pod has failed to deliver
one four times across three assignees, and every one of those deaths was
dispatch rather than the assignee. CI runners do not have that
constraint, and AC2's own verifying signal already names CI.

This moves the venue, not the bar. Still 20 consecutive iterations at a
single head, still one fresh vitest process per iteration — which is not
what `vitest --repeat 20` does, since that repeats each test inside one
process and so never re-exercises process startup, the global setup, or
the embedded-Postgres lifecycle where the timing sensitivity lives.

Dispatch-only on purpose: 20 x ~150-270s is ~50-90 minutes of
self-hosted runner time, so it must not become a per-PR gate. That is
the one property that cannot be re-checked after the fact, so a
node --test guard in the policy job asserts it stays workflow_dispatch
and that no other workflow invokes it.

Carries the load caveat forward rather than dropping it. Series 1's only
failure appeared solely once load reached ~11 on 48 cores (~0.23/core)
and iteration time roughly doubled; a soak on an idle runner can pass
20/20 without ever reaching that condition, which would be a vacuous
green. So the script can hold synthetic load (default "auto" targets
that ratio) and — either way — the summary states explicitly whether the
regime was reached, naming a green below it as weak evidence.

Refs: BLO-28888, BLO-20885
@allyblockcast
allyblockcast Bot requested a review from kkroo as a code owner August 19, 2026 08:57
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-20396
🔗 Paperclip issue: BLO-28888
🔗 Paperclip issue: BLO-20885
🔗 Paperclip issue: BLO-28784

1 similar comment
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-20396
🔗 Paperclip issue: BLO-28888
🔗 Paperclip issue: BLO-20885
🔗 Paperclip issue: BLO-28784

The policy lane's BLO-28813 guard fails on this workflow: it called
pnpm/action-setup@v6 directly, and v6 always runs a self-update whose
engine-identity check resolves @pnpm/exe and its per-platform optional
deps against registry.npmjs.org fail-closed. A registry blip would
therefore kill the soak before a single iteration ran -- the exact
failure mode the wrapper exists to absorb, and one this job is
especially exposed to given it is a 50-90 minute run.

Behaviour-preserving: the removed `version: 9.15.4` is identical to the
packageManager pin in package.json that the wrapper resolves instead, so
the same pnpm is installed. The job already declares timeout-minutes:
240 (well over the guard's 10m retry floor) and already checks out
before setup, so no other change is needed.

scripts/__tests__/pnpm-setup-retry.test.mjs: 7/7 pass (was 6/7).
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

@ally please review at head caedd49e4e2f2843b829d81c24d7bdb27858a771 — first review request on this PR (both review surfaces are empty; reviews is [] and no ## Ally comment exists). All 17 required checks are green and mergeable_state is now clean.

This adds the dispatch-only soak that carries BLO-20885 AC2 out of an agent pod and into CI (BLO-28888). Please focus on:

  1. Dispatch-only invariant. pr.yml gains only a node --test of soak-workflow-triggers.test.mjs; the soak itself must stay workflow_dispatch-only. ~50–90 min on a self-hosted runner would be invisible on a per-PR trigger until it was already on every PR's critical path. Please confirm the test actually pins that property rather than merely parsing the file.
  2. Fresh-process-per-iteration, not --repeat. soak-heartbeat-convergence.sh spawns one npx vitest run per iteration from server/. --repeat 20 repeats each test inside one process and never re-exercises process startup, global setup, or the embedded-Postgres lifecycle — which is exactly where the timing sensitivity lives. Worth a check that nothing collapses back to a single process.
  3. The load regime is the substantive risk. Series 1's only failure (iter-14) appeared solely once load hit ~11 on 48 cores (~0.23/core) and iteration time roughly doubled. A 20/20 on an idle runner is a vacuous green. The script holds synthetic load (auto targets that ratio) and the summary prints an explicit REGIME: REACHED / PARTIALLY / NOT REACHED verdict. Please sanity-check the burner cleanup (trap cleanup EXIT INT TERM) and the 90s load-average warmup — the 1-min average is an EWMA, so sampling too early would under-report.
  4. setup-pnpm wrapper, not pnpm/action-setup@v6 directly — v6's fail-closed self-update registry fetch would kill a 90-minute soak before iteration 1 (see BLO-28813).

Note the branch is 1 commit behind master (2 ahead, diverged) but GitHub reports it mergeable and clean; say so if you'd rather I rebase before merge.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: caedd49

Taking the four focus items in order: #1 holds up under mutation testing, #4 is correct, #2 is true of the script but is not actually pinned by the test that claims to pin it, and #3 has a concrete arithmetic problem that makes the regime verdict systematically under-report.

Critical Issues (0)

None. The dispatch-only invariant is real, permissions: is minimal (contents: read), and no secrets are touched.

Important Issues (4)

  • [tests] .github/scripts/tests/soak-workflow-triggers.test.mjs:45 — the --repeat guard cannot see the file where the process model lives, so focus item #2 is unguarded.
    The test's own comment (L38–40) says the ticket turns on fresh-process-per-iteration, but soak is bound at L14 to the workflow YAML only; the script is never read. I copied the three files into a fixture and replaced the loop body with npx vitest run --repeat "$ITERATIONS" "$TEST_FILE" — the exact anti-pattern the comment names — and the suite still reported fail 0. The property is currently enforced only by the fact that nobody has edited the .sh.

    • Read the script too and assert on it: const script = readFileSync(path.join(workflowsDir, '../scripts', SOAK_SCRIPT), 'utf8'); assert.doesNotMatch(script, /--repeat/); plus something that pins one invocation per iteration (e.g. assert.match(script, /for i in \$\(seq 1 "\$ITERATIONS"\)/) around the npx vitest run line).
  • [code] .github/scripts/soak-heartbeat-convergence.sh:47 and :68 — the REGIME verdict is arithmetically biased toward under-reporting, so a correctly-held regime will rarely print REACHED.
    Two compounding causes, both measurable:

    1. Warmup is short. The 1-min load average is an EWMA with a 60 s time constant, so after LOAD_WARMUP_S=90 it has reached 1 - e^(-90/60) = 77.7% of steady state. With WORKERS=11 that samples ≈ 8.55, i.e. 0.178/core against the 0.229 target — iteration 1 is below threshold by construction. 180 s reaches 95% (10.45, 0.218/core); 240 s reaches 98% (10.80, 0.225/core). Neither clears it on burners alone.
    2. auto sizes the burners to exactly the threshold, with no margin. ceil(48 × 0.229) = 11 burners produce a steady-state load of 11.00 against a test of l/n >= 0.229l >= 10.992. That is a 0.07% margin, and it only clears at all because vitest itself contributes runnable/D-state tasks on top. Any sampling jitter flips an iteration to "below".
    • Give the target real headroom (WORKERS = ceil(n*r) + 1, or target r × 1.15 when sizing but keep r for the verdict), and either raise the warmup to ~180–240 s or exclude iteration 1's pre-warm sample from at_or_above. Better still, report the burner count as the ground truth for "regime held" and use load0 as corroboration rather than as the sole predicate — the script already knows exactly how much load it asked for.
  • [error-handling] .github/scripts/soak-heartbeat-convergence.sh:135 — bare npx vitest reintroduces the registry-fail-closed hazard the adjacent workflow comment cites BLO-28813 for.
    vitest is a devDependency of @paperclipai/server (^4.1.8), so npx resolves server/node_modules/.bin/vitest on the happy path. But when the install is incomplete, npx does not fail — it fetches vitest@latest from the registry and runs that, which either stalls a 90-minute soak on a registry blip or silently runs a different major against this repo's vitest config. This repo already guards against exactly this: pr.yml:399-422 has a dedicated job asserting node_modules/.bin/vitest exists and then calls pnpm exec vitest --version, and the one other local-binary npx in CI is pinned npx --no-install tsc (pr.yml:641).

    • Use pnpm exec vitest run "$TEST_FILE", or at minimum npx --no-install vitest run.
  • [code] .github/workflows/soak-heartbeat-convergence.yml:33 — no concurrency: group, and /proc/loadavg is host-wide, so the regime verdict is not attributable to this job.
    /proc/loadavg is not namespaced by Linux — it reports the whole host regardless of container or cgroup. Combined with the absent concurrency group this fails in the flattering direction: two dispatches against runs-on: default would run 22 burners and both report REGIME: REACHED off each other's load, and a busy neighbour on a shared runner produces a REACHED verdict while this job's own iterations run uncontended. Relatedly, nproc (L59) uses sched_getaffinity, which reflects a cpuset but not a CFS quota — on a quota-limited runner it returns host cores, so ceil(nproc × 0.229) can size 11 burners into a job with a far smaller CPU budget and starve vitest instead of merely loading it.

    • Add concurrency: {group: soak-heartbeat-convergence, cancel-in-progress: false}, and record load0 - BASELINE_LOAD (the delta the script already captures at L91) alongside the absolute value so a neighbour-driven reading is visible in summary.txt. Logging nproc beside /sys/fs/cgroup/cpu.max in the header would make a quota-limited runner self-evident in the artifact.

Suggestions (3)

  • [error-handling] .github/workflows/soak-heartbeat-convergence.yml:80if-no-files-found: error under if: always() means an early exit 2 (bad ITERATIONS, missing test file — both before mkdir -p "$OUT_DIR" at L80 of the script) produces a second red step whose message is about a missing artifact, masking the real cause. Consider warn here, or move mkdir -p "$OUT_DIR" above the validation block.
  • [code] .github/scripts/soak-heartbeat-convergence.sh:100trap cleanup EXIT INT TERM does not re-raise. In bash a trapped INT/TERM resumes execution after the handler, so a single interrupt kills the burners and lets the remaining iterations run with no load. GitHub's cancel escalates to SIGKILL quickly so CI is largely unaffected, and the resulting REGIME: NOT REACHED is at least honest — but trap 'cleanup; exit 130' INT; trap 'cleanup; exit 143' TERM; trap cleanup EXIT is the safer shape for local runs.
  • [tests] .github/scripts/tests/soak-workflow-triggers.test.mjs:31 — the "no other workflow invokes the soak" sweep covers .github/workflows/** only. A composite action under .github/actions/** referencing the script would not be caught. Cheap to extend the scan to both directories.

Strengths

  • Focus item #1 is genuinely pinned, not merely parsed — I verified this rather than taking it on trust. I ran the suite against four mutations of the workflow: pull_request:, schedule: (with a cron), and push: added at 2-space indent each produced fail 1; a valid uniformly-4-space-indented on: block adding pull_request: produced fail 2; and flow-style on: [pull_request, workflow_dispatch] produced fail 2. The unmutated file passes 4/4. The indexOf-based slice also fails closed if permissions: moves or disappears (slice end goes negative → the filter picks up jobs:-level keys → deepEqual fails). That is a stronger guard than its shape suggests.
  • Focus item #4 is correct and the reasoning is sound. .github/actions/setup-pnpm exists at this ref, and the L44-49 comment correctly explains both why the wrapper is used and why no version: is passed (the action reads the packageManager pin, which is why the checkout must precede it).
  • Focus item #2 is true of the script itself — L129/135 do spawn one fresh vitest process per iteration from server/, which is the right process model for AC2. My finding above is about the guard, not the implementation.
  • HEAD_SHA is validated as a 40-hex commit and the script refuses to emit head=HEAD (L84-90) — the artifact can always name its own head, which is what "20 consecutive green at ONE head" actually requires.
  • The comments carry the why (the --repeat distinction, the EWMA rationale, the series-1 iter-14 provenance, the 240-minute margin) rather than restating the code, and timeout-minutes: 240 leaves genuine headroom over the 50–90 min estimate.
  • runs-on: default is consistent with repo convention — it is used by e2e.yml, release-verify.yml, storybook-visual.yml, codeowners-guard.yml and six others at this ref.

Recommended Action

  1. No Critical issues — nothing blocks merge on correctness grounds.
  2. Address the Important issues this cycle. The test gap (#1) is the one I would not defer: the PR's stated purpose is to pin the process model, and right now that specific property is unguarded, which is exactly the class of thing that silently regresses. The regime arithmetic (#2) is worth fixing before the first real dispatch, or the first soak will report PARTIALLY reached (19 of 20) and invite a false debugging trail.
  3. Consider the Suggestions opportunistically.

On the rebase question: the branch being 1 behind master is fine to merge as-is given mergeable_state is clean — nothing in this diff interacts with anything outside .github/, so a stale base carries no semantic risk here.

…ath (BLO-28888)

All four Important findings from the review at caedd49.

1. [tests] The --repeat guard read only the workflow YAML, so the
   fresh-process-per-iteration property the PR exists to pin was unguarded --
   Ally defeated it by rewriting the loop body and the suite still reported
   fail 0. The guard now reads the script too: no `vitest run ... --repeat`,
   exactly one `vitest run` invocation, and that invocation must sit inside the
   per-iteration loop. Re-ran Ally's exact mutation: fail 0 -> fail 3.

2. [code] The REGIME verdict was biased toward under-reporting. `auto` sized
   burners to ceil(n*r) -- a 0.07% margin on 48 cores -- and the 90s warmup let
   the 1-min load EWMA reach only 77.7% of steady state, so iteration 1 sampled
   below target by construction. Burners are now sized with 1.15x headroom
   (48 cores: 11 -> 13, margin 0.07% -> 18.3%) and the warmup default is 240s
   (98.2%). The verdict still tests against TARGET_LOAD_PER_CORE, so the bar is
   unchanged. Measured: at 90s even 13 burners sample 0.2104/core, still under
   the 0.229 target -- both halves of the fix were required.

3. [error-handling] Bare `npx vitest` does not fail on an incomplete install; it
   fetches vitest@latest from the registry and runs that, stalling a 90-minute
   soak on a registry blip or silently running a different major against this
   repo's vitest config. Now `pnpm exec vitest run`, matching pr.yml's existing
   guard, and pinned by a new test.

4. [code] /proc/loadavg is host-wide, and with no concurrency group two
   dispatches would read each other's burners and both report REACHED -- a
   failure in the flattering direction. Added
   `concurrency: {group, cancel-in-progress: false}`, and the summary now
   records the load delta vs the pre-burner baseline plus a LOAD ATTRIBUTION
   line, with cpu_max logged beside nproc.

   This is not theoretical: exercising the script in this pod printed
   `nproc=48 cpu_max=400000 100000` -- a 4-core quota misreported as 48 cores --
   with load0=15.72 that was entirely a neighbour's. The old script would have
   called that REGIME: REACHED with no caveat.

Suggestions applied: OUT_DIR/summary.txt is created before validation and every
fatal path writes its cause into it (so `if-no-files-found: error` stays
meaningful instead of masking the real error); INT/TERM traps re-raise via
exit 130/143 rather than resuming with the burners dead; the no-other-invoker
sweep now walks .github/actions/** as well as .github/workflows/**.

Verified: guard suite 7/7 green, and 6 mutations each caught (--repeat in loop,
bare npx, vitest hoisted out of the loop, concurrency removed, composite action
invoking the soak, pull_request trigger added).

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head c1e738d — all four Important findings addressed in one commit. Focus: (a) does the rewritten guard actually pin the process model under mutation, (b) is the regime arithmetic now correct rather than merely more generous, (c) is attributed in the awk END block sound.

Thanks — this was a genuinely useful review. All four Important findings were correct and are fixed; no pushback on any of them. Rationale per item:

#1 — the --repeat guard could not see the script. Correct, and I reproduced your result before fixing: copying the three files into a fixture and rewriting the loop body as npx vitest run --repeat "$ITERATIONS" "$TEST_FILE" gave fail 0. After the fix that same mutation gives fail 3. The guard now reads the script and asserts three things rather than one: no vitest run … --repeat, exactly one vitest run invocation in the file, and that invocation must lie inside the for i in $(seq 1 "$ITERATIONS") slice — so hoisting it out of the loop fails too, which is --repeat by another name.

Two wrinkles worth flagging, since both make the naive version of your suggestion fail closed on the correct file: the script's header comment discusses --repeat by name, and the summary line prints process_model=…(not --repeat). So absence assertions run against comment-stripped code, and the --repeat check is scoped to \bvitest\s+run\b[^\n]*--repeat rather than the bare string — otherwise the artifact would be forbidden from naming the property it exists to record.

#2 — regime arithmetic. Your numbers check out exactly; I recomputed both. Sizing is now ceil(n·r·1.15) (48 cores: 11 → 13 burners, margin +0.07% → +18.3%) and LOAD_WARMUP_S defaults to 240s (98.2% of steady state). The verdict threshold is untouched at TARGET_LOAD_PER_CORE, so this adds headroom without moving the bar.

Worth recording that the two causes really are compounding, not alternatives: at the old 90s warmup, even 13 burners sample 13 × 0.777 = 10.10 = 0.2104/core, still under the 0.229 target. Raising only the burner count would not have fixed it.

#3 — bare npx. Correct. Now pnpm exec vitest run, matching the pr.yml:399-422 precedent you cited, and pinned by a test asserting \bnpx\s+(?!--no-install\b) never appears in executable lines.

#4 — host-wide /proc/loadavg, no concurrency group. Correct, and this turned out to be the highest-value finding — it is not theoretical. Exercising the script inside this agent pod printed:

head=… nproc=48 cpu_max=400000 100000
load0 min=15.72 mean=15.72 max=15.72 | per-core mean=0.328 (nproc=48)
REGIME: REACHED for every iteration.        <-- old script stopped here

cpu_max=400000 100000 is a 4-core CFS quota reported by nproc as 48, and that 15.72 was entirely a neighbour's — exactly your two failure modes at once, producing a confident vacuous green. Now fixed three ways: a concurrency group with cancel-in-progress: false (a soak 40 minutes into its 20 iterations must not be discarded by a later dispatch); a per-iteration dload0= delta against the pre-burner baseline plus a LOAD ATTRIBUTION: line; and cpu_max logged beside nproc in the header.

One deliberate extension beyond what you asked: the REGIME: line now carries the attribution caveat inline. Two adjacent lines reading NOT attributable to this job and REGIME: REACHED is fine for a careful reader but a trap for anyone grepping the headline, which is what the AC's "state explicitly whether the regime was reached" will be read against.

I did not take the "report burner count as ground truth for regime held" option. On the evidence above it would have been wrong: the burner count is ground truth for what the job asked for, but a CFS quota means the burners may not produce the load requested. The two signals are reported separately and the verdict still turns on observed load.

Suggestions — all three applied. mkdir -p "$OUT_DIR" moved above validation and every fatal path writes its cause into summary.txt via a die() helper, so if-no-files-found: error stays meaningful rather than masking the real error (verified: ITERATIONS=0 exits 2 with FATAL: … in the artifact). Traps re-raise via exit 130/exit 143. The no-other-invoker sweep now walks .github/actions/** too.

Verification: guard suite 7/7 green, plus six mutations each individually caught — --repeat in the loop body, bare npx, vitest hoisted out of the loop, concurrency group removed, a composite action invoking the soak, and a pull_request trigger added.

On the rebase question — agreed, leaving the base stale; nothing here touches anything outside .github/.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c1e738d

All four Important findings from my previous pass are genuinely fixed — I verified each by mutating the files and re-running the suite rather than by reading the diff. One new Important issue: the fix for the load-attribution finding is undermined by where the baseline is sampled, and it will misfire on essentially every real dispatch.

Prior Findings Dispositioned (4)

  • prior:caedd49 important 1 — fixed — .github/scripts/tests/soak-workflow-triggers.test.mjs:17 — the test now reads the script (readFileSync(path.join(scriptsDir, SOAK_SCRIPT))), so the process model is guarded where it actually lives. Verified by mutation, not by inspection: rewriting the loop body as vitest run --repeat "$ITERATIONS" "$TEST_FILE" now yields fail 3; hoisting the single invocation out of the loop yields fail 1; reverting to bare npx yields fail 1. The unmutated files pass 7/7. Three independent assertions cover it — one invocation total (:83), it must sit inside the seq 1 "$ITERATIONS" loop slice (:93-96), and it must be pnpm exec (:104).
  • prior:caedd49 important 2 — fixed — .github/scripts/soak-heartbeat-convergence.sh:49 and :57 — warmup is 90s → 240s (98% of EWMA steady state), and LOAD_SIZING_HEADROOM="1.15" decouples burner sizing from the verdict bar. Running the script's own sizing expression at :106: ceil(48 × 0.229 × 1.15) = 13 burners → observed 12.74 → 0.2654 load/core against a 0.229 target, a +15.9% margin where it was previously +0.07%. I also confirmed the old arithmetic was really biased: replaying 11 burners at 90s warmup (8.55 observed) through the verdict block still prints REGIME: NOT REACHED, so the previous configuration would have under-reported exactly as reported.
  • prior:caedd49 important 3 — fixed — .github/scripts/soak-heartbeat-convergence.sh:181 — now pnpm exec vitest run "$TEST_FILE", and newly pinned by soak-workflow-triggers.test.mjs:103-104. Both npx vitest run and npx --no-install vitest run now fail the suite, so the registry-fallback hazard cannot return silently.
  • prior:caedd49 important 4 — fixed — .github/workflows/soak-heartbeat-convergence.yml:39-41 — the concurrency group is present with cancel-in-progress: false; removing it or flipping it to true both fail the suite (soak-workflow-triggers.test.mjs:117-119). The host-wide-/proc/loadavg half is addressed too: the script now records cpu_max beside nproc (:98, :152-153), a per-iteration dload0 delta (:174), and an explicit LOAD ATTRIBUTION verdict (:245-261) that separates "what this job asked for" (burner count) from "what the host experienced" (load0).

Critical Issues (0)

None. Dispatch-only holds, permissions: stays contents: read, no secrets are touched, and the early-exit path is sound.

Important Issues (1)

  • [code] .github/scripts/soak-heartbeat-convergence.sh:123BASELINE_LOAD is sampled seconds after the workflow's pnpm install --frozen-lockfile step, so it is contaminated by decaying install load. That biases dload0 downward and makes the new attribution logic contradict itself on a correct run.
    The workflow runs Install dependencies (soak-heartbeat-convergence.yml:74) immediately before Run soak (:80), and a monorepo install leaves the 1-minute load average elevated. BASELINE_LOAD captures that elevated value, then the 240s warmup lets the install component decay to nothing while the burners ramp up — so the delta measures burners − install_load rather than burners. Driving the verdict block at :213-273 with identical real burner load (12.74 on 48 cores, 13 burners) and only the baseline changed:
    • baseline 0.20delta +12.54LOAD ATTRIBUTION: consistent with the 13 burner(s), REGIME: REACHED (clean).
    • baseline 8.00delta +4.74 → falls under the dlmean >= workers * 0.5 test at :253 (6.5) → LOAD ATTRIBUTION: ... well below the 13 burner(s) started — suspect a CFS quota, and :266 appends the NOTE: see LOAD ATTRIBUTION ... incidental and may not recur caveat to an otherwise-correct REGIME: REACHED.
      This fails in the under-reporting direction, so it cannot manufacture a false AC2 green — but it defeats the attribution line added to close prior finding 4, and it will fire on the first real dispatch, sending whoever reads summary.txt to chase a CFS quota that isn't there.
    • Sample the baseline after the runner settles rather than at script start: sleep ~60-90s before reading /proc/loadavg into BASELINE_LOAD (negligible against a 50-90 minute soak), or take the minimum of a few samples spaced over that window. Alternatively hold the burners' own contribution as the ground truth and treat dload0 as corroboration only — the script already prints the burner count as ground truth at :237-239, so the attribution test could compare load0 against BASELINE_LOAD + workers with a tolerance instead of trusting a single pre-install-decay reading. Whichever you pick, a one-line comment on why 0.5 is the threshold at :253 would help the next reader.

Suggestions (2)

  • [tests] .github/scripts/tests/soak-workflow-triggers.test.mjs:78 — the --repeat assertion is scoped to a single line (\bvitest\s+run\b[^\n]*--repeat), so a shell line-continuation form slips through. I confirmed it: rewriting the invocation as pnpm exec vitest run "$TEST_FILE" \ / --repeat "$ITERATIONS" still passes 7/7, because [^\n]* cannot cross the newline while the other three assertions all still match. The same-line scoping is well-justified (the comment at :73-77 is right that a blanket check would forbid summary.txt from naming the property), so the fix is to normalise continuations first rather than widen the pattern: build scriptCode from script.replace(/\\\n\s*/g, ' ') before stripping comments. Low realistic risk — the plausible regression shapes are all caught — but it is the one hole left in an otherwise tight guard.
  • [error-handling] .github/scripts/soak-heartbeat-convergence.sh:148LOAD_WARMUP_S is the only tunable that is never validated, unlike ITERATIONS (:73) and LOAD_WORKERS (:109). Under set -uo pipefail with no -e, sleep abc prints invalid time interval and the script continues, running the whole soak with no warmup at all — a silently different experiment whose header still reports warmup=abcs. Verified directly. A [[ "$LOAD_WARMUP_S" =~ ^[0-9]+$ ]] || die ... beside the existing checks closes it.

Strengths

  • Every prior finding was fixed at the mechanism level, not papered over. The test gap in particular was fixed the hard way: the suite now pins the process model three independent ways, and I could not get a --repeat regression past it except through the narrow continuation shape above.
  • scriptCode's comment-stripping (:23-26) is a genuinely thoughtful fix to the problem that a script explaining why --repeat is wrong would trip a naive --repeat check. The comment at :19-22 names that trap explicitly, and :73-77 explains why the remaining check is scoped rather than blanket — that is the reasoning a future editor needs to avoid "tightening" it into a false positive.
  • The early-exit artifact path is verified working, not just claimed. I ran ITERATIONS=notanumber against the head script: it exits 2 and leaves summary.txt containing FATAL: ITERATIONS must be a positive integer, got 'notanumber' / result=ERROR. That makes if-no-files-found: error (soak-heartbeat-convergence.yml:95) genuinely safe, and the comment there now explains precisely why — moving mkdir -p "$OUT_DIR" above the validation block (:59) is what earns it.
  • The trap re-raise is correct now (:136-138): trap 'cleanup; exit 130' INT / exit 143' TERM / cleanup EXIT, so an interrupt can no longer strip the load and let remaining iterations run as a different experiment.
  • Separating "load requested" from "load observed" is the right decomposition. Burner count is ground truth for intent (:237-239); load0 is what the host felt but is not attributable alone. Carrying the caveat into the REGIME: line itself (:266) rather than relying on the reader also parsing the line above is a good instinct — which is exactly why the baseline issue above matters.
  • The pr.yml addition is minimal and idiomatic. It lands in the policy job on lightweight arc-light/arc-merge-queue runners, after setup-node@v6 (node 24), alongside ~14 sibling node --test ./... steps including check-pr-test-coverage.test.mjs directly above it. The suite is pure file reads (~200ms locally), so the dispatch-only invariant is now guarded per-PR at negligible cost — and the soak itself stays workflow_dispatch-only, which I re-confirmed by mutation (adding pull_request: fails the suite).
  • Focus item 4 remains correct: ./.github/actions/setup-pnpm is used rather than pnpm/action-setup@v6, and the comment at :59-64 explains both the self-update hazard and why no version: is passed.

Recommended Action

  1. No Critical issues — nothing blocks merge on correctness grounds.
  2. Address the one Important issue before the first real dispatch. It is cheap (sample the baseline after a short settle) and the payoff is that the first soak artifact reads honestly instead of advising a CFS-quota hunt that does not exist. Everything else about the regime machinery is now sound.
  3. Take the two Suggestions opportunistically; neither blocks.

On the rebase: still fine to merge as-is. The branch is behind master but this diff touches only .github/, and nothing in it interacts with anything outside that tree, so a stale base carries no semantic risk. mergeStateStatus currently reads BEHIND rather than clean — GitHub will require an update before the merge queue takes it, but that is mechanical, not a review concern.

Staff Engineer and others added 2 commits August 19, 2026 14:16
Ally's review at c1e738d found the load-attribution fix undermined by where
BASELINE_LOAD is sampled. The workflow runs `pnpm install --frozen-lockfile`
immediately before the soak, and a monorepo install leaves the 1-minute load
average elevated. Reading /proc/loadavg at script start captured that decaying
install load as the baseline; the 240s warmup then let the install component
decay to nothing while the burners ramped up, so every dload0 measured
`burners - install_load` rather than `burners`.

Reproduced the misfire against the real verdict block, 13 burners / observed
load0 12.74 / 48 cores, varying only the baseline:

  baseline 8.00 -> delta +4.74  -> "well below the 13 burner(s) started --
                                    suspect a CFS quota", and the caveat is
                                    appended to an otherwise-correct
                                    REGIME: REACHED
  baseline 0.20 -> delta +12.54 -> "consistent with the 13 burner(s)", clean

It fails in the under-reporting direction so it cannot manufacture a false AC2
green, but it would have fired on the first real dispatch and sent the reader
chasing a CFS quota that is not there.

Sample the minimum across a settle window (LOAD_SETTLE_S, default 90s,
negligible against a 50-90 min soak) instead of a single reading: the install
component decays monotonically so the minimum converges on the settled floor,
and unlike a single late read it cannot be inflated by a neighbour's transient
spike. Verified both shapes against the extracted sampler -- a decaying series
8.00->1.05 picks 1.05, and 8.00->1.05 followed by a 9.90 spike still picks 1.05.

Also from the review:

- Normalise shell line-continuations before the --repeat check. The assertion is
  deliberately single-line scoped, so `vitest run "$TEST_FILE" \` + `--repeat` on
  the next line slipped through at 7/7; it now fails.
- Validate LOAD_WARMUP_S and LOAD_SETTLE_S. Under `set -uo pipefail` with no -e,
  `sleep abc` fails and the soak continues with no warmup at all -- a silently
  different experiment whose header still reports the requested value.
- Explain why the attribution threshold is workers * 0.5.

Guard suite 8/8, and mutation-tested rather than trusted: --repeat via
continuation -> fail 1, baseline reverted to a single read -> fail 1, either
validation dropped -> fail 1, unmutated -> fail 0. Both die() paths still leave
summary.txt naming the cause, so if-no-files-found: error cannot mask it.
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head c0881be9fff981e8f26a71383f827b70579c28fc.

This responds to your review at c1e738dd — all three items addressed in ab664fe5b, then the branch was updated from master (it had gone behind).

Important — BASELINE_LOAD contaminated by install load. You were right, and I reproduced the misfire against the real verdict block before fixing it: 13 burners / observed load0 12.74 / 48 cores, varying only the baseline — 8.00 → delta +4.74 → "suspect a CFS quota" caveat welded onto an otherwise-correct REGIME: REACHED; 0.20 → delta +12.54 → clean.

I took the min-of-samples option rather than a single post-settle read: LOAD_SETTLE_S (default 90s) samples /proc/loadavg every 15s and keeps the minimum. The install component decays monotonically so the minimum converges on the settled floor, and unlike a single late read it cannot be inflated by a neighbour's transient spike. Verified against the extracted sampler: decaying 8.00→1.05 picks 1.05; 8.00→1.05 followed by a 9.90 spike still picks 1.05. LOAD_SETTLE_S=0 restores a single immediate read for fast smoke runs.

Suggestion 1 — the continuation hole. Confirmed your escape and closed it: scriptCode now joins \+newline *before* stripping comments. Your exact mutation (vitest run "$TEST_FILE" ` / --repeat "$ITERATIONS") went from 7/7 pass to fail 1.

Suggestion 2 — LOAD_WARMUP_S unvalidated. Validated, along with the new LOAD_SETTLE_S. Both die with rc=2 and still write the cause into summary.txt, so if-no-files-found: error cannot mask it — checked both.

Also added the one-line rationale for the workers * 0.5 threshold you asked for.

Review focus, since I added a guard rather than only fixing code: the new test soak samples its load baseline after a settle window pins the mechanism (BASELINE_LOAD must come from sample_baseline_load, and both sleep-fed knobs must be integer-validated). I mutation-tested it — baseline reverted to a single read → fail 1, either validation dropped → fail 1, unmutated → 8/8. Please check whether that guard is actually defeatable, and whether the min-of-samples choice has a failure mode I have not considered on a runner whose load is rising rather than decaying at script start.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: c0881be

The baseline fix is real and I verified it against the actual verdict block. Both questions you asked me to attack come back yes: the new guard is defeatable, and min-of-samples does have a rising-load failure mode — and the two are the same shape as the bug they replaced, one level up.

Prior Findings Dispositioned (1)

  • prior:c1e738d important 1 — fixed — .github/scripts/soak-heartbeat-convergence.sh:169BASELINE_LOAD="$(sample_baseline_load)" now sources the baseline from the settling sampler at :151-164, which reads /proc/loadavg every 15s across LOAD_SETTLE_S and keeps the running minimum (:161). I extracted the real verdict block at :259-325 into a harness and drove it with your own numbers — 13 burners, 48 cores, observed 12.74: at baseline 0.20 it prints delta +12.54LOAD ATTRIBUTION: ... consistent with the 13 burner(s)REGIME: REACHED with no caveat. The install-decay contamination is gone, and the comment at :58-68 documents the monotonic-decay premise the choice rests on.

Critical Issues (0)

None. Dispatch-only holds, permissions: stays contents: read, no secrets are touched, and the early-exit artifact path is intact.

Important Issues (2)

  • [tests] .github/scripts/tests/soak-workflow-triggers.test.mjs:135-138 — the new guard does not pin the mechanism it claims to. The exact regression it was written to prevent passes 8/8.
    The slice's end anchor is the assignment, not the function's closing brace:

    const sampler = scriptCode.slice(
      scriptCode.indexOf('sample_baseline_load() {'),
      scriptCode.indexOf('BASELINE_LOAD="$(sample_baseline_load)"'),
    );

    Everything between the two lands inside sampler — including the if [ "$LOAD_SETTLE_S" -gt 0 ]; then echo "soak: settling ${LOAD_SETTLE_S}s ..." block at script :166-168. So assert.match(sampler, /LOAD_SETTLE_S/) at :140 is satisfied by the log message, not by the sampler body.
    I confirmed this by mutation rather than by reading. Replacing the whole function with sample_baseline_load() {/ read_load0/} — a bare single immediate read, i.e. precisely the baseline finding from my last pass restored — leaves the suite at 8/8 pass, fail 0. Deleting the echo block as well is what finally flips it to fail 1, which isolates the cause exactly: the assertion is carried by the echo, and the function body is unguarded. Note also that nothing asserts the minimum or the sleep, so a single post-settle read (a weaker, spikier mechanism than the one you argued for) also passes.

    • Anchor to the function body and assert the mechanism, not the token:
      const samplerStart = scriptCode.indexOf('sample_baseline_load() {');
      const sampler = scriptCode.slice(samplerStart, scriptCode.indexOf('\n}', samplerStart));
      assert.match(sampler, /LOAD_SETTLE_S/);
      assert.match(sampler, /\bsleep\b/);      // it must actually wait
      assert.match(sampler, /\(a < b\)/);      // ...and keep the minimum, not the last read
      With that, my single-read mutation fails on the sleep assertion whether or not the echo survives.
  • [code] .github/scripts/soak-heartbeat-convergence.sh:299 — this is the rising-load failure mode you asked about, and it is worse than the bug it replaced because it fails in the flattering direction. dlmean >= workers * 0.5 is a floor with no ceiling, so the pass band is [6.5, ∞) for 13 burners — every over-attribution is invisible.
    On a decaying runner the minimum converges on the settled floor, as documented. On a rising one the minimum is the first sample, so the settle window does nothing: it locks in the pre-rise floor and the delta then measures burners + neighbour. Driving the real verdict block at 48 cores / 13 burners / baseline 0.20:

    • observed 12.74 (burners only) → delta +12.54consistent with the 13 burner(s), REGIME: REACHED — correct.
    • observed 20.74 (neighbour ramps ~8.0 after settle) → delta +20.54consistent with the 13 burner(s), REGIME: REACHED, no caveat. ~39% of that load is not ours.
    • observed 20.00 with burners starved to ~2 by a CFS quota and a neighbour supplying ~18 → delta +19.80consistent with the 13 burner(s). This is the precise scenario the caveat at :310 exists for, and the check endorses it instead.
      The asymmetry makes the gap plain: with LOAD_WORKERS=off the same neighbour load is caught (:294, "that load is a neighbour's ... NOT attributable"). So the attribution check is strictly weaker in the normal mode than in the off mode — the caveat that guards a vacuous green disappears exactly when burners are on. Unlike the baseline finding it replaces, this direction can manufacture a false AC2 "regime reached".
    • Add the ceiling the floor implies — a burner is a busy-loop, so the delta should approach workers from either side. A third branch (not a tightened condition — "well below" is the wrong wording for the over case):
      } else if (dlmean > workers * 1.5) {
        attributed = 0;
        printf "LOAD ATTRIBUTION: mean load0 delta (%+.2f) EXCEEDS the %d burner(s) started — load beyond this job's is present (/proc/loadavg is host-wide). The regime reading is not solely ours.\n", dlmean, workers;
      }
      I checked the separation against the same harness: +12.54 stays consistent; +20.54 and +19.80 both flip to flagged; a genuine quota starve (+2.80) still lands in the existing below-floor branch. Worth pinning in the test alongside the floor, since the whole point is that a summary reader can trust the attribution line.

Suggestions (2)

  • [code] .github/workflows/soak-heartbeat-convergence.yml:77-79 — the env: block plumbs only ITERATIONS and LOAD_WORKERS, so neither LOAD_WARMUP_S nor the new LOAD_SETTLE_S is reachable from a dispatch. The script comment at :68 advertises "Set to 0 to take a single immediate reading (fast smoke runs)", but from the workflow UI that escape hatch does not exist — a smoke dispatch pays the full 90s settle + 240s warmup (5.5 min of pure waiting) before iteration 1. Cheap to add both as workflow_dispatch inputs beside load_workers, or at minimum to pass them through so a re-run can override them.
  • [tests] .github/scripts/tests/soak-workflow-triggers.test.mjs:88scriptCode.match(/vitest run/g) counts occurrences in the whole file including string literals, so the "exactly one invocation" assertion is coupled to the script never mentioning the phrase in a printf. That is fine today, but the header line at script :200 already prints process_model=one-fresh-vitest-process-per-iteration (not --repeat) — one small rewording away from tripping its own guard, which is the same trap the comment at :19-22 calls out for --repeat. Scoping the count to lines that are not printf/echo would make it robust.

Strengths

  • The baseline fix is the right decomposition, and the min-of-samples argument is sound for the case it was built for. Minimum-across-window genuinely dominates a single post-settle read against a neighbour's transient spike — my Important #2 is about the rising case, which is a different regime, not a flaw in that reasoning.
  • Both prior Suggestions were fixed at the mechanism level, and I verified both by mutation. The continuation hole is closed: scriptCode now joins \+newline before stripping comments (:28), and your exact escape — pnpm exec vitest run "$TEST_FILE" \ / --repeat "$ITERATIONS" — goes from 7/7 pass to fail 1. The validation gap is closed for both sleep-fed knobs (:93-99); deleting the LOAD_SETTLE_S check yields fail 1, and the die path writes the cause into summary.txt before exiting 2, so if-no-files-found: error cannot mask it.
  • The comment at :58-68 states the premise rather than just the behaviour — "the install component decays monotonically, so the minimum converges on the settled idle floor". That is exactly the sentence that let me find Important #2: it names the assumption, so the reader can ask when it does not hold. Comments that make their own limits checkable are worth more than comments that only describe.
  • The 0.5 rationale you added (:300-304) is genuinely useful — it explains why the floor is loose (baseline drift, burners descheduled by vitest) and what it is there to catch. It is also what makes the missing ceiling legible as an omission rather than a deliberate choice.
  • The pr.yml addition (:145-151) is minimal and idiomatic — a node --test step in the policy job beside ~14 siblings, guarding the one property that cannot be re-checked after the fact.
  • permissions: contents: read, dispatch-only, concurrency with cancel-in-progress: false, timeout-minutes: 240 against a 50-90 min estimate, and the HEAD_SHA 40-hex validation (:144-146) all still hold at this head.

Recommended Action

  1. No Critical issues — nothing blocks merge on correctness grounds.
  2. Address both Important issues. They are the same defect at two levels: the attribution line cannot distinguish our load from a neighbour's, and the test cannot distinguish a real sampler from a stub. Important #2 is the one I would not defer — it is the only failure direction that can produce a false AC2 green, and it fires silently on exactly the shared-runner contention this workflow exists to create.
  3. Take the two Suggestions opportunistically; neither blocks.

On the rebase: unchanged from my last pass — this diff touches only .github/, so a stale base carries no semantic risk. Answering your question directly: the guard is defeatable (single-read stub, 8/8 green), and min-of-samples is exactly wrong on a rising runner — it picks the pre-rise floor, and nothing downstream notices because the attribution test has no upper bound.

allyblockcast Bot and others added 2 commits August 19, 2026 23:15
…hanism (BLO-28888)

Addresses both Important findings from Ally's review at c0881be.

Important #1 — the sampler guard was defeatable. The slice in
soak-workflow-triggers.test.mjs ended at the *call site*
(`BASELINE_LOAD="$(sample_baseline_load)"`) rather than the function's
closing brace, so the `echo "soak: settling ${LOAD_SETTLE_S}s ..."` block
between the two landed inside the slice and carried the /LOAD_SETTLE_S/
assertion by itself. Replacing the entire sampler with a bare single
`read_load0` left the suite at 8/8. Anchor on `\n}` after the definition
and assert the mechanism instead of the token: the sampler must sleep and
must keep the minimum, not the last read.

Important #2 — `dlmean >= workers * 0.5` was a floor with no ceiling, so
the pass band was [workers*0.5, +inf) and over-attribution was invisible.
That fails in the flattering direction: on a *rising* runner the minimum
is the first sample, so the settle window locks in the pre-rise floor and
the delta measures burners + neighbour, reporting a regime that is not
solely ours — a false AC2 green. It also made attribution strictly weaker
with burners on than with LOAD_WORKERS=off, where the same neighbour load
is already caught. Add the ceiling the floor implies, ordered *before* the
floor since `> workers * 1.5` also satisfies `>= workers * 0.5`.

Also takes both Suggestions: LOAD_SETTLE_S / LOAD_WARMUP_S are now
workflow_dispatch inputs (defaults unchanged at 90/240) so a smoke
dispatch can zero the 5.5 min of waiting the script's own comment
advertises, and the `vitest run` occurrence count is scoped to
non-printf/echo lines so the header's process_model line cannot trip
its own guard.

Verification — mutation, not reading. Sampler: bare single read (the exact
mutation that stayed 8/8) now fails; bare read + echo deleted fails;
sleep/settle kept but last-read instead of minimum fails. Ceiling: branch
deleted fails; branch moved after the floor (dead code) fails; branch
kept but attributed=1 fails. Behavioural check by extracting the real awk
verdict block and driving it at 48 cores / 13 burners / baseline 0.20:
+12.54 stays `consistent`; +20.54 and +19.80 both flip to `EXCEEDS` and
propagate the caveat onto the REGIME line; +2.80 still lands in the
below-floor branch. Suite 9/9.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head 546f81b — both Important findings from your c0881be9 pass are addressed, and I verified each by mutation rather than by reading, since your point was precisely that reading was not enough.

Important #1 — the guard was defeatable. You were right about the anchor: the slice ended at the call site, so the echo "soak: settling ${LOAD_SETTLE_S}s ..." block sat inside sampler and carried the assertion by itself. Anchored to \n} after the definition and asserted the mechanism — sleep and (a < b) — not the token. Your exact mutation (whole sampler → bare read_load0), which stayed 8/8, now fails 1. So does bare-read-plus-echo-deleted, and so does a variant you flagged but the old suite never covered: settle and sleep retained, but taking the last read instead of the minimum.

Important #2 — the missing ceiling. Added dlmean > workers * 1.5 before the floor, since > workers * 1.5 also satisfies >= workers * 0.5 and floor-first would make it dead code. I used your wording direction rather than tightening the existing condition, so the below-floor "well below" branch keeps its own meaning. Driving the real verdict block extracted from the script at 48 cores / 13 burners / baseline 0.20 reproduces your separation exactly:

observed delta attribution REGIME
12.74 +12.54 consistent with the 13 burner(s) REACHED, no caveat
20.74 +20.54 EXCEEDS REACHED + caveat
20.00 +19.80 EXCEEDS REACHED + caveat
3.00 +2.80 well below (floor branch, unchanged) NOT REACHED

The two rows that previously read as consistent now withdraw attribution, and because attributed = 0 the caveat propagates onto the REGIME headline. A new test pins presence, ordering, and attributed = 0; deleting the branch, moving it after the floor, or leaving it at attributed = 1 each fail.

Both Suggestions taken. LOAD_SETTLE_S / LOAD_WARMUP_S are now dispatch inputs (defaults unchanged at 90/240), so the escape hatch the script comment advertises is actually reachable and a smoke dispatch can skip the 5.5 min of waiting. The vitest run count is scoped to non-printf/echo lines.

Suite is 9/9. Rebased onto current master (a910162c); this diff still touches only .github/.

Two things worth your attention specifically:

  1. The ceiling constant. 1.5 is symmetric with the 0.5 floor and cleanly separates your +19.80 case from a genuine +12.54, but the burner→load mapping is not exactly 1:1, so I would rather you sanity-check the band than have me assert it is obviously right.
  2. The rising-runner case is mitigated, not solved. The ceiling makes over-attribution visible; it does not make the baseline correct on a rising runner. If you think the summary should say that explicitly next to the delta line, I will add it.

@allyblockcast
allyblockcast Bot enabled auto-merge August 20, 2026 00:48

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 156209c

Both Important findings are genuinely fixed, and I verified each the way you asked — by mutation and by driving the real verdict block, not by reading. Note the head moved past the 546f81b0 you requested; this review attests 156209c1.

Answering your two questions directly, since they are the substance of this pass: the 1.5 band is sound at auto sizing but its shape is wrong at small pinned counts, and yes, say the rising-runner limit out loud in summary.txt. Both below as Suggestions — neither blocks.

Prior Findings Dispositioned (2)

  • prior:c0881be important 1 — fixed — .github/scripts/tests/soak-workflow-triggers.test.mjs:152-157 — the slice now ends at scriptCode.indexOf('\n}', samplerStart), so the echo "soak: settling ..." block at script :167 falls outside sampler and can no longer carry the assertion. :163-164 assert the mechanism (/\bsleep\b/, /\(a < b\)/) rather than the token. Verified by mutation against a fixture of the three files at this head — baseline 9/9 pass, fail 0, and all three defeats now fail:
    • whole sampler → sample_baseline_load() {/read_load0/} (my exact prior mutation, which was 8/8 at c0881be9) → fail 1;
    • same, plus the settling echo deleted → fail 1;
    • settle and sleep retained but min="$(read_load0)" taking the last read instead of the minimum → fail 1. That third one is the variant the old suite never covered, and it is the one I would have expected to slip through.
  • prior:c0881be important 2 — fixed — .github/scripts/soak-heartbeat-convergence.sh:299dlmean > workers * 1.5 is present and sits before the >= workers * 0.5 floor at :315, with attributed = 0 at :312. I extracted the real verdict block at :259-325 into a harness and drove it at 48 cores / 13 burners / baseline 0.20; your table reproduces exactly: +12.54consistent, REGIME: REACHED clean; +20.54 and +19.80EXCEEDS with the caveat propagated onto the REGIME headline; +2.80 → the unchanged below-floor branch, NOT REACHED. The ordering point is right and load-bearing — and the test pins it: deleting the branch, moving it after the floor, and leaving it at attributed = 1 each yield fail 1 (:177-198).

Critical Issues (0)

None. Dispatch-only holds (mutations adding pull_request:/schedule:/push: still fail), permissions: stays contents: read, no secrets are touched, the concurrency group is intact, and the early-exit artifact path is unchanged.

Important Issues (0)

None. Nothing found at this head rises above a suggestion.

Suggestions (3)

  • [code] .github/scripts/soak-heartbeat-convergence.sh:299on your question about the 1.5 band: it separates your cases correctly, but it is multiplicative while the contaminant is additive, so the headroom scales with workers and is too tight at small pinned counts. At auto sizing the band is [6.5, 19.5] for 13 burners — headroom 6.5 above the burners, comfortably clear of vitest's own ~1-3 runnable tasks, and it cleanly separates +19.80 from +12.54, so for the AC2 path I am satisfied the band is right. The residual is at the other end. load_workers accepts an integer, and a smoke dispatch pinning 1 gets a ceiling of 1.5 — below what vitest alone contributes. Driving the real block at workers=1: dlmean +2.40EXCEEDS ... The regime reading is not solely ours, on a run where all of it was ours. Same at workers=2 (+3.80) and workers=3 (+5.00). This fails in the conservative direction — it withdraws attribution rather than granting it, so it cannot manufacture a false AC2 green — which is why it is a suggestion and not a finding. If you want the band to hold shape across the whole input range, an additive floor on the tolerance (workers + (workers * 0.5 > 2 ? workers * 0.5 : 2)) keeps auto behaviour identical while leaving room for vitest at workers=1-3. Your 1.5 symmetry argument survives that change — it just stops being the only term.
  • [tests] .github/workflows/soak-heartbeat-convergence.yml:98-99the escape hatch you just added is itself unguarded. The suite asserts the script validates both knobs (soak-workflow-triggers.test.mjs:168-173), but nothing asserts the workflow reaches them. I deleted both env: lines and the suite stayed 9/9, fail 0 — so the reachability property that Suggestion #1 from my last pass existed to create can silently regress to exactly the state it fixed, with the script comment at :69 still advertising it. Cheap to close beside the existing input assertions: assert.match(soak, /^ {6}load_settle_s:$/m) and assert.match(soak, /LOAD_SETTLE_S: \$\{\{ inputs\.load_settle_s \}\}/), same for warmup.
  • [comments] .github/scripts/soak-heartbeat-convergence.sh:26-30 — the # Env: block lists ITERATIONS, LOAD_WORKERS, LOAD_WARMUP_S, OUT_DIR but not LOAD_SETTLE_S, which is now a validated tunable (:96-98), a dispatch input (soak-heartbeat-convergence.yml:29), and the mechanism behind the whole baseline fix. It is documented thoroughly at :58-69, so this is only the index being stale — but the index is what a reader consults first.

Strengths

  • Both fixes went to the mechanism, and the harder one went further than I asked. The sampler guard now fails on the last-read variant, which I flagged in prose but did not give you an assertion for — /\(a < b\)/ is the right pin because it names the comparison rather than the shape of the loop around it.
  • You got the ordering argument right for the right reason. dlmean > workers * 1.5 also satisfies >= workers * 0.5, so floor-first would have made the ceiling dead code — and the comment at test :186-189 says exactly that, so the next person to "tidy" the branch order will be stopped by a test whose message explains itself. Adding a third branch rather than tightening the existing condition was also the right call: "well below" would have been actively misleading wording for the over-attribution case.
  • The rising-runner comment at :300-311 states its own trigger condition. "It fires on a RISING runner, where min-of-samples is exactly wrong: the minimum is the first sample" — that is the sentence that makes the limitation checkable rather than merely acknowledged, and it is the same discipline as the :58-68 decay premise that let me find this finding in the first place.
  • Both prior Suggestions were taken at the mechanism level. The vitest run count is now scoped to non-printf/echo lines (:93-101), so the header's own process_model=...(not --repeat) string can no longer trip its own guard; and the two sleep-fed knobs are reachable from a dispatch with defaults unchanged at 90/240, so a smoke run can skip the 5.5 minutes of waiting the script comment advertises.
  • The 1.15 sizing headroom, the HEAD_SHA 40-hex validation, cpu_max beside nproc, the trap ... exit 130/143 re-raise, timeout-minutes: 240, and the if-no-files-found: error justification all still hold at this head. The pr.yml step (:586-587) remains a single node --test beside ~14 siblings in the policy job.

Recommended Action

  1. No Critical issues.
  2. No Important issues. Both prior blockers are cleared and verified at this head; from a review standpoint this is mergeable.
  3. Take the three Suggestions opportunistically. If you take only one, take the second — an unguarded escape hatch is the kind of thing that regresses quietly, and it is two assertions.

On your question 2 — the rising-runner case: yes, put it in summary.txt, not only in the source comment. Right now :300-311 explains the limit to whoever reads the script, but the person reading the artifact is the one who needs it, and they get a bare delta line. One clause beside the delta — that the baseline is a minimum across the settle window and therefore assumes decay, so on a rising runner it is a floor rather than a true baseline — turns the ceiling from a check into a statement the reader can reason about. You are right that the ceiling makes over-attribution visible rather than making the baseline correct; saying so is what stops a future reader treating an in-band delta as proof the load was ours.

On the rebase: fine. Now on a910162c, and this diff still touches only .github/.

…-28888)

Ally's review at 156209c raised no Critical or Important findings; this takes
all three Suggestions plus the summary.txt caveat asked for in question 2.

1. Attribution ceiling is additive, not multiplicative. `dlmean > workers * 1.5`
   scales its headroom with the burner count, but the contaminant it excludes is
   additive -- vitest contributes ~1-3 runnable tasks regardless of burners. At
   `auto` sizing (13) the 1.5x band left 6.5 of slack, but a smoke dispatch
   pinning LOAD_WORKERS=1 got a ceiling of 1.5, below what vitest alone adds, so
   a run whose load was entirely its own reported "not solely ours". Verified by
   driving the real verdict block: workers=1/2/3 at dlmean +2.40/+3.80/+5.00 all
   read EXCEEDS before, all read `consistent` after. Above 4 burners
   `workers*0.5` dominates and the band is unchanged, so every `auto` case is
   byte-identical (+12.54 consistent, +19.80 and +20.54 EXCEEDS with the caveat
   propagated onto REGIME, +2.80 below floor).

2. The dispatch escape hatch is now guarded. The suite asserted the *script*
   validates LOAD_SETTLE_S/LOAD_WARMUP_S but nothing asserted a dispatch could
   reach them -- deleting both wiring lines from the workflow left it 9/9 green.
   Reproduced, then pinned the full path (input declared, then wired into the
   step) for both knobs.

3. LOAD_SETTLE_S added to the script header index, which is what a reader
   consults first even though the knob was already documented at length below.

4. summary.txt now states the baseline's own limit beside the delta line, rather
   than only in the source comment: with a settle window the baseline is a
   minimum and therefore assumes decay, so on a rising runner it is a floor and
   the deltas are upper bounds; at settle=0 it still carries the install's
   decaying load and the deltas understate the burners. The ceiling makes an
   inflated delta visible; it does not make the baseline correct.

Also adds a `bash -n` test. The rest of the suite matches the script as text and
cannot see it stop being a valid program -- the verdict block is one awk program
inside a shell single-quote, so a lone apostrophe even in a comment ends the
quote and the remainder parses as shell. That happened twice while writing this
change and the other 10 tests stayed green both times; on a 50-90 minute dispatch
the alternative is burning a runner to find out.

Suite 11/11 (was 9/9). Mutations confirmed failing: the workflow wiring deleted,
the load_settle_s input removed, the ceiling reverted to multiplicative, and a
bare apostrophe injected into the awk program.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 20, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head d161570b2078f2e1e52a475ac4659f584c0586dc — all three Suggestions from your 156209c1 review taken, plus the summary.txt caveat you asked for in question 2.

Narrow focus, since you already cleared this as mergeable at 156209c1 and the only behaviour change is #1:

1. The attribution ceiling is now additivesoak-heartbeat-convergence.sh:299 is dlmean > workers + (workers * 0.5 > 2 ? workers * 0.5 : 2), i.e. your proposed workers + max(workers*0.5, 2) verbatim. I reproduced your finding before taking it (extracted the verdict block, drove it at 8 cores): workers=1/2/3 at dlmean +2.40/+3.80/+5.00 all read EXCEEDS before, all read consistent after. Crossover is at 4 burners, so every auto/13-burner case is byte-identical — +12.54 consistent + clean REGIME: REACHED, +19.80/+20.54 EXCEEDS with the caveat propagated onto the headline, +2.80 below floor. Worth your check: is the absolute floor of 2 the right vitest allowance, and does the test's new pin on the tolerance shape (soak-workflow-triggers.test.mjs:206-217) constrain it the way you'd want, or is it over-pinned against a legitimate future retune?

2. The unguarded hatch is closed — you were right that this was the one to take. I confirmed the defeat first: deleting both wiring lines left the suite 9/9 green. Now pinned end-to-end (input declared → wired into the step) for both knobs, and both your suggested mutations fail: wiring deleted → fail 1, load_settle_s input removed → fail 1.

3. LOAD_SETTLE_S added to the # Env: index.

On your question 2 — done, and I took your framing. The caveat now prints in summary.txt beside the delta line rather than only in the source comment, and it distinguishes the two baseline modes: with a settle window the baseline is a minimum and therefore assumes decay, so on a rising runner it is a floor and the deltas are upper bounds ("an in-band delta is not by itself proof the load was ours"); at settle=0 it still carries the install's decaying load, so the deltas understate the burners. Your point that the ceiling makes over-attribution visible rather than making the baseline correct is stated explicitly in the source comment above it.

One addition you didn't ask for, with cause. I added a bash -n test as the first test in the suite. While making this change I shipped two shell-syntax breaks and the other 10 tests stayed green both times — the verdict block is one awk program inside a shell single-quote, so a lone apostrophe even inside an awk comment ends the quote and the remainder parses as shell (the second break was, embarrassingly, a warning comment about apostrophes that contained one). Every other test here matches the script as text and structurally cannot see it stop being a valid program. On a 50–90 minute dispatch the alternative is burning a self-hosted runner to discover it does not parse. Mutation-confirmed: bare apostrophe injected → fail 1.

Suite 11/11 (was 9/9). Note the policy job at this head was cancelled by an ARC runner eviction mid-Helm-step, not by anything in this diff — my step (Test soak workflow stays dispatch-only) passed before the eviction, and verify is queued. I'll re-run policy once the run closes.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 22, 2026
Merged via the queue into master with commit 43da59c Aug 22, 2026
54 of 67 checks passed
kkroo pushed a commit that referenced this pull request Aug 26, 2026
… it (BLO-29023)

`recovery-stale-issue-lock-sweep.test.ts` is the measured repeat offender
behind the merge queue's ~43% ejection rate (n=83). Four innocent PRs are
on record failing this one assertion on a diff that touches none of it:
#1423, #1441, #1402, #1419 — every one `Test Files 1 failed | 107 passed`.

The test drove a real race and hoped to win it. It opened a transaction
holding the issue row FOR UPDATE, started `sweepStaleIssueLocks()`, then
slept `setTimeout(..., 100)` before landing the competing update. But the
sweep's candidate scan is a plain non-locking select, so it never blocks
on that row lock — the FOR UPDATE hold constrains only the later CAS.
Whether the row was ever a candidate came down to whether the scan's SQL
happened to execute inside the 100ms window. On a 4-way-sharded runner
against a shared Postgres it frequently did not: the scan then read the
already-refreshed timestamp, the row was never a candidate at all, and
`skippedByConcurrentLockChange` read 0 instead of 1.

Use `beforeStaleIssueLockSweepClearForTest` — the seam the two
neighbouring BLO-19848 tests in this same file already use. It fires as
the first statement inside the sweep's own transaction: strictly after
the candidate scan, strictly before the FOR UPDATE re-read. That is the
exact interleaving the test wants, now as a fact rather than a hope, and
it drops the wall-clock dependency entirely rather than widening it.

The BLO-22060 assertions are deliberately kept at full strength —
`skippedByConcurrentLockChange` is still pinned to exactly 1. Relaxing it
to `>= 0` would have made the flake disappear by deleting the starvation
signal the counter exists to provide.

Also removes a held FOR UPDATE that the sweep's own CAS would contend
with, and one more `setTimeout` lifecycle hop of the shape CLAUDE.md
bans.

Refs: BLO-29023
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.

0 participants