Skip to content

fix(ci): close the cache saturation that makes the matrix non-deterministic - #80

Merged
guysenpai merged 19 commits into
mainfrom
ci/cache-saturation-hotfix
Sep 14, 2026
Merged

fix(ci): close the cache saturation that makes the matrix non-deterministic#80
guysenpai merged 19 commits into
mainfrom
ci/cache-saturation-hotfix

Conversation

@guysenpai

Copy link
Copy Markdown
Contributor

Dedicated CI hotfix, before M1.2.1. Touches .github/workflows/ci.yml and
nothing else, and is validated by its own runs — which is why it is not folded
into an animation milestone's review.

The cause is in the key

All three cache keys carry ${{ github.sha }}, so no save is ever
overwritten
: each commit creates two fresh archives per cell and restoration is
necessarily a prefix match on the previous lineage. Growth is monotone in the
number of commits pushed — two entries × fifteen cells per run, eight of them
windows at 1–2 GB — against GitHub's repository-wide 10 GB ceiling. It is not
brushed by accident; it is reached by construction in a handful of commits, and
LRU eviction takes over.

Measured, run 34777902366 at 5d79846:

cell attempt 1 attempt 2, same sha
windows-2025, ReleaseSafe, f64 57 min, cancelled (budget 55) 9.6 min ✓
windows-2025, Debug, f64 21 min, cancelled (budget 20) 5.6 min ✓
windows-2025, ReleaseSafe, f32 52 min ✓ 52.1 min

Every step of both cancelled cells succeeded, Complete job included — so
neither is M1.D.19 (tests LOST, test runner failed to respond) nor
M1.D.29 (an assertion falling). Restore keys attribute it exactly: e60dda8…
then 4ad6487…. The f32 cell passed on attempt 1 and restored the stale
lineage on attempt 2, its own save having been evicted meanwhile — the cells
evict each other inside one run.
Repository cache read live: 11.55 GB / 13
entries, then 9.44 GB / 11 a few minutes later; the four windows cells held
8.44 GB of 9.44, 89 % of the budget for 4 of 15 cells.

So this is not a slow CI, it is a non-deterministic one. The re-run cleared
it by luck.

What lands here

Point 1 — the -build save is deleted, with its now-dead measurement step
(exactly one consumer). Measured: the inherited -build entries hold 3.69 GB
today. Its unique window was a job killed between it and the final save — the
timeout, which saturation causes: it existed to survive an ill it fed. And
ci.yml already recorded it killing a cell outright, Save Zig cache (post-build) going 39s → 5m23s → 7m39s on windows-2025 / Debug while the
useful work stayed flat. The all-or-nothing doctrine was moved, not deleted.

Point 2 — 55 → 75 and 20 → 35. Not comfort: a budget's job is to separate
SLOW from HUNG, and at 55 against a cold cell measured at 52–57 it separated
nothing. The Debug budget was the tighter of the two in proportion — 21 measured
against 20 allocated — and no debt entry had ever named it.

Two hypotheses refuted, one per party

That the milestone had outgrown its budget: the previous run carried all
three hotfix commits and both new tests and did the same cell in 10.6 min
two tests do not cost 46 minutes. And that M1.2.0/G1 widened the blast radius
via type_info.zig importing foundation: measured, command_buffer.zig and
scheduler.zig already did before the milestone and
core_module.addImport("foundation") dates from M1.1.14 — breaking that one
dependency would change nothing.

Not in this push

Saving only from main (point 3) — the gesture that closes the cause rather
than the symptom, but the only one whose cost is not known in advance; it owes a
before/after measurement on the same branch.

Splitting the cell by test domain is refused for now — the tidiest-looking
lever and the only one that aggravates the root cause, since splitting
multiplies cache entries.

How to read this run

The volume effect is partly masked: the inherited -build archives survive until
LRU retires them. What is immediately readable is that this run writes one
entry per cell instead of two, and that no cell is cancelled with every step
green.

Detail: briefs/ci-hf-cache-saturation.md. M1.D.27 is mis-diagnosed by its own
entry — cause, not number — and Guy rewrites it under the same number.

🤖 Generated with Claude Code

guysenpai and others added 19 commits September 13, 2026 13:30
The frozen section as produced by Claude.ai, with the eight specs of the
reading list ticked off in the living section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Mat4(T)` is declared in `src/foundation/math/mat4.zig` and `rtti` names it
by alias, which preserves the type IDENTITY `classifyField` matches on. Its
storage is `extern struct { m: [16]T }` and not `Mat3`'s `cols: [N]Vec`,
because it carries a frozen C twin that a `@Vector` layout cannot serve.

The twin had no assertion at all — `WeldMat4` has zero use sites and was tied
to `rtti.Mat4` by a doc comment — so a field-by-field comparison lands with a
witness that it can return false.

`affineInverse` dotted the column where the formula wants the row; a diagonal
fixture cannot tell the two apart, the rotated non-uniform one did.

The pose layout is decided on the bench, not on intuition: SoA-3 wins blend
AND forward kinematics at 32/64/128 bones over three runs. The per-channel
form, the only one that vectorises, loses the blend by 75% — which refutes
the stated argument for SoA while confirming SoA. An instrument defect was
corrected first: the AoS body was written scalar against a vector SoA body,
and equalising it moved the 32-bone ratio from 0.936x to 1.006x at identical
checksums.

Test floor 2290 -> 2300 / 2298, re-derived from the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The three layouts differed in two things at once — memory order AND element
representation — so the gap could belong to either. `AoS-vec` holds both
fixed but the order: same 48 bytes, same alignment, `@Vector` members.

It reverses the verdict. Interleaved beats split on blend 12 times out of 12
across four runs and ties it on FK, so SoA-3 is dominated. What the earlier
reading had credited to the split was the `[3]f32` element: the corpus's own
AoS candidate, one Tier 0 `Transform` per bone, is the slowest viable form by
about 10% on both operations, an axis neither document named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`src/interfaces/AnimationModule.zig` declares the types its own signatures
name and attests its non-freeze with a test that fails the day a protocol
version appears. The assert block covers what an implementation owes today
and grows with it: asserting an entry nothing can satisfy forces a body that
returns a plausible answer, which hides worse than an absence.

`init` reaches `world` and `persistent_allocator` and nothing else, registers
the `Skeleton` component there, and registers no system — a system with no
body to run is a mechanism nothing executes.

Poses live outside the ECS and the component carries a handle. Instance slots
are never recycled: an id is a position, so a reused slot would make a stale
handle address a different skeleton and read as working. Counter-factual run:
recycling reddens exactly that test and no other.

Test floor 2300 -> 2315 / 2313.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loading refuses a malformed hierarchy rather than repairing one: a loader
that reroots an orphan or clamps an index makes every downstream invariant
untestable. The topological order is checked here, which makes a cycle
unrepresentable rather than detected — a cycle must contain a forward
reference and a forward reference is what this refuses, so the two are one
check and one code.

`error.NoRoot` was written and removed: the test for it failed, and reading
why showed no input reaches it. Bone zero has no index below it, so it is the
root or the hierarchy was already refused. An error nobody can provoke is an
assertion, and it is now one, with the argument at the site.

The inverse bind pose is stored, not derived. Deriving would force the bind
pose and the rest pose to coincide; they legitimately differ, which is why
glTF ships both.

Test floor 2315 -> 2329 / 2327.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pass produces MODEL space — each bone relative to the skeleton root — and
never world: the entity transform enters at the consumers, and baking it here
would make the skin matrices and the socket reads both wrong in a way that
looks right at the origin.

Rig and instance are two stores, so one hierarchy backs many poses. A fresh
instance stands in its bind pose: a skeleton at the identity is a heap of
bones at the origin and reads as a sampling bug.

The single pass is asserted twice and neither is timing — the topological
invariant read off the loaded rig, and the fixed point, since a second pass
would move something if a parent were evaluated after its child.

A coverage hole found by counter-factual: reversing the rotation operands
left the suite green, because the acceptance chain rotates about one axis and
its leaf carries the identity, and quaternions commute in both cases. Closed
with two non-parallel rotations, a sequential-application oracle, and a
discrimination guard.

Test floor 2329 -> 2339 / 2337.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The attack was written first and it caught the plausible implementation: a
profile mapping twenty-two of twenty-three roles, the gap in the middle. A
resolver indexing the mapping array by ordinal answers foot_l's bone for the
unmapped calf_l — a neighbour, which is the failure the invariant names — and
the positive test stays green under it, because a resolver right for
twenty-two roles passes every ordinary test.

Two bones of one name are refused at load. Otherwise a name addresses two
bones and the resolver has to pick, which is acting on possibly the wrong one.
It caught a real duplicate on its first run.

The counter-proof corpus pins the addressing type: a bare string, a bare index
and a bare ordinal must not compile in a reference position, while both
legitimate variants must. Both halves were shown to redden — and the first
attempt to show it was a bad counter-factual that moved the guard one level in
instead of removing it.

Test floor 2339 -> 2348 / 2346.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bindModelPose` had no caller and no test, duplicated what `instantiate` does
inline, and carried an unstated aliasing contract — it passed one buffer as
both input and output of the kinematics pass.

`resolveRole` and `resolveName` were public beside a header claiming
`resolveBone` is the one path. Two public entries that map a role or a name
are two more paths, in the module's own surface, at the place the invariant
is written down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deviations, blockers, the closing self-audit and the final measurements.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three defects in shipped behaviour. `Mat4.approxEql` compared NaN EQUAL —
`@abs(x-y) > tol` is false for NaN — so four assertions could not fail on a
total failure of the inverse. `rig()` returned a pointer the next `loadRig`
frees, which the shipped scenario holds across its whole `main`. Neither new
process entry installed the float environment, and the bench is the one whose
published numbers decided the layout.

Eight guards that could not fail, each verified by mutation. The two "single
linear pass" tests passed against a kinematics reduced to a copy; the
`id == len` boundary was never probed; three refusals of `parse` were
unreachable from `parse`; the C-twin's negative witness differed in width, so
the field loop it exists for never ran.

`fromTrs`'s third column was pinned by nothing, and the first repair was blind
to two columns for a different reason — a quarter turn makes their diagonal
terms exactly zero. What ships is a half turn plus a generic rotation checked
against `rotateVec3` plus a guard that refuses a fixture with a near-zero
basis term. All nine terms now detected.

And the class this milestone had already closed twice, committed while closing
it: `resolveName` stayed public under a header saying it was not.

Scope: the module now publishes a resource and registers one `fixed_update`
system, driven through a real `dispatchFrame`; the seven unimplemented
interface entries are present and refuse; `parse` validates its float payload.

Test floor 2348 -> 2357 / 2355.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Thirteen cells green. The windows ReleaseSafe f64 cell cleared its
55-minute budget by 45 seconds, which is the cost this brief predicted for a
gate touching a depended-upon Tier 0 header.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The same two cells took 49m40 and 54m15 cold, then 4m28 and 4m26 warm — a
factor of 11 to 12 on a branch whose only intervening change is one line of
journal. The 54 minutes were compilation, not test execution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by external review after this milestone's own adversarial pass had
closed. `compose` built a model-space `BoneTransform` from two others, and
that composition is exact for the position and WRONG for the scale: it
multiplies the two scales componentwise, which is the right answer only
when no rotation between the two bones reorients the scale axes.

MEASURED before the fix, on a root scaled `(3, 1, 1)` with a quarter-turned
child: the pass put the grandchild at `(0.0000, 3.0000, 0.0000)` where the
matrix product puts it at `(0.0000, 1.0000, 0.0000)`. Wrong BY THE SCALE
FACTOR, finite and plausible, on a rig no artist would call unusual.

What let it through is written at `skeleton.zig`'s header: a comment on that
composition documented an approximation about SHEAR — genuinely
inexpressible in TRS — and a documented approximation on one quantity does
not cover an error on another. It reads as an argument, which is this
repository's costliest defect class.

`ModelPose` is declared on the interface, `compose` is deleted,
`forwardKinematics` composes `Mat4` and the local poses stay TRS — a clip
samples translation, rotation and scale, and nothing is gained by widening
what it writes. `pose.zig` gains `allocModel`/`freeModel`; `root.zig`,
the tests, the demo and the layout bench follow.

The pin is a test named for the defect: an accumulated scale must not
survive a rotated child. Its oracle is the matrix product, computed by
walking each bone up to the root — not the pass's own arithmetic, which
would agree with itself whatever it did.

AND THE BENCH'S FK ARM STOPPED DISCRIMINATING, which is reported rather
than quietly re-run: it measured a TRS output, and against `[]Mat4` the
ratios straddle 1.0 (0.870-1.153) with the sign flipping between bone
counts inside one run, matrix composition dominating the read cost. The
layout verdict stands on the BLEND arm, unchanged and stable across three
runs. Recorded in the bench header and in `bench/results/pose_layout.md`,
where the FK row is marked superseded rather than left to read as current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by external review after this milestone's own adversarial pass had
closed. `validatePayload` refused a stored rotation whose length missed one
by more than `unit_rotation_tolerance` and did nothing to the ones it
admitted — so a quaternion legitimately inside the band stayed off-unit,
the matrix built from it stretched, and the stretch COMPOUNDED down the
hierarchy onto a consumer that had been told the payload was validated.

The tolerance is NOT tightened. It exists to refuse the absurd — a null
quaternion, an arbitrary one — and it keeps that job; what it never was is
a guarantee of unit length. The two run in order, and the order is
load-bearing: a null norm NORMALISED is a NaN, so the refusal must come
first. Both halves are pinned, and the counter-factual that inverts them
reddens two tests with `expected error.NonUnitRotation, found
error.NonFiniteTransform` — the NaN arriving exactly where the argument
says it would.

THE FIRST ATTACK WRITTEN FOR THIS MEASURED ALMOST NOTHING AND IS THE
REASON THE FIX IS WHERE IT IS. A 128-deep chain at half the tolerance
passed at 1.000321, so rather than escalate the fixture blindly the
exposure was PROBED: drift is governed by the rotation ANGLE and not by
the norm error alone. At the band's edge a 1.2 rad per-bone rotation
reaches 8.5 % stretch over 128 bones (5.7 % about (1,1,1)); the same error
at 0.1 rad reaches 0.06 %. The first attack used 0.1 rad, which is why it
proved the opposite of what it claimed. Tightening the band would not have
closed this.

The attack ships at the measured worst case, and it asserts BOTH halves:
the stored rotation is unit AFTER LOAD — the direct property, which does
not depend on a chain length — and the FK tip is unit-length to 1e-3 after
128 compositions. Its non-vacuity is established in the same execution: the
same chain rebuilt from the unnormalised quaternion is required to stretch
past 1.05, so the assertions above it are about the normalisation and not
about a fixture that could never have drifted.

Suite floor re-derived FROM THE SUITE: 2357 -> 2359 on macOS, 2355 -> 2357
on windows, the closure agreeing independently at 2359.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by external review after this milestone's own adversarial pass had
closed. `rig()` returned a `Rig` BY VALUE, and that copy carries the store's
own slice handles: `borrowed.parents[1] = 127` wrote into the hierarchy the
loader had validated, and `borrowed.deinit(gpa)` freed the module's memory
while the store went on holding the same slices. Both compiled. Both were
silent.

THE PREVIOUS REMEDY IS WHAT PRODUCED THIS ONE, and it is the finding's real
content. `rig()` first returned a `*const Rig`, which dangled the moment
another rig was loaded, and the repair changed the SHAPE of the return —
pointer to value — which closed the dangle and left ownership exactly where
it was. A remedy that changes the form of a return moves the defect instead
of closing it; what was needed was a second TYPE.

So: two types, not one. `Rig` stays the owner — mutable slices, `deinit`,
internal to the store, and its doc now says that is what it is. `RigView`
is what leaves: every slice `[]const`, no destructor, and no
`profile_mappings` at all, that field being the backing storage `deinit`
frees and having no meaning to a reader. `resolveBone` takes the view too —
resolution is a READ and has no business receiving a destructor.

The counter-proof is a NON-COMPILATION, in the corpus that already drives
one: `case-view-write` fails with `cannot assign to constant` and
`case-view-deinit` with `no field or member function named 'deinit' in
'skeleton.RigView'`. The first mark is generic in isolation, so it is paired
with its own file name — a mark that any constant assignment anywhere would
satisfy discriminates nothing. The control compiles in the same execution
and now READS through the view: bone count, bone name, parents, bind pose,
inverse bind and the profile. Without that half, two refusals would prove
the type useless rather than tight.

AND THE HARNESS IS SHOWN TO FIRE. With `rig()` restored to returning a
`Rig`, both new cases COMPILE and the corpus reddens: `process exited with
code 0 (expected exited with code 1)`, twice. The two lines this commit
makes impossible were legal code on the branch that was about to be tagged.

The corpus keeps its addressing name for a second family rather than
growing a rename: the path is prescribed by the brief's frozen section, and
a cosmetic divergence from it is still a divergence. What is NOT claimed is
that no code can name the owning record — Zig has no module-private
declaration. What is checkable, and checked, is that the owning type never
appears in an entry's return type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hotfix was reviewed GO MERGE with one objection: quaternion norm is
multiplicative, so |q1*...*qn| = (1+e)^n independent of angle, therefore
"drift is governed by the angle" is false and the two fixtures must have had
norm errors 100x apart.

MEASURED, both objects in one execution, delta measured after construction
rather than declared, over 128 bones. The quaternion-product norm is FLAT to
five figures across angles 0.1 to 3.0 — 1.066054 to 1.066059 — while the
matrix chain at the same delta runs 1.000641 to 1.289770. The reviewer's
theorem is exact (Euler's four-square identity, verified over 200000 random
pairs against this repo's own Quat.mul at 7.6e-16) and their arithmetic is
right at every step. It governs Quat.mul, which src/modules/kinesis/ calls
ZERO times. forwardKinematics composes Mat4, and Mat4.fromTrs writes an
unscaled 1 on each diagonal term while every off-diagonal term carries the
full |q|^2 — so M(k*u) = k^2*R(u) + (1-k^2)*I, an affine mix with the
identity, verified elementwise to 1 ULP. The decisive cell is theta = 0:
delta fully present, stretch exactly 1.000000, where a law independent of
angle demands 1.066.

BUT THE OBJECTION WAS RIGHT ABOUT THE SENTENCE IT ATTACKED. "Tightening the
band would not have closed it" is FALSE — a non-sequitur, since the drift is
the product n*delta*(1-cos theta) and a product closes by shrinking any
factor; 100x tighter takes the 128-bone stretch from 8.5% to 0.08%. What is
true and stronger is that the band is defeated by DEPTH: max_bones is 4096,
and at the band's own edge such a chain stretches 13.6x at 1.2 rad and 3583x
at pi. Both false sentences are deleted here, not annotated.

AND THE FIXTURE WAS ADMISSIBLE BY ONE ULP AND BY LUCK — a second defect the
objection's wrong number nevertheless landed on. k = sqrt(1 + tolerance)
names a length the band REFUSES: 1 + 1e-3 is unrepresentable in f32 and
rounds up to 8389 ULP where the predicate admits 8388. It loaded only because
five roundings happened to land one ULP low at that angle; the identical
construction is refused at 15.2% of angles in (0,3], non-monotonically —
1.80 rad refused between an accepted 1.50 and an accepted 2.00.

Repaired, and the attack is STRONGER: 7550 ULP under the 8388 ceiling against
a 2 ULP construction spread, 0% of angles refused, 3.0 rad instead of 1.2,
unnormalised stretch 1.257397 instead of 1.085067, floor 1.05 -> 1.2. The 0.9
factor is written as a rule (factor <= 1 - N*2^-23/tol) and not as a number,
because it stops working at a tighter tolerance. rotationIsUnitEnough is
extracted from validatePayload as the ONE declarant of the band arithmetic,
whose summation order decides the last ULP. The in-test assertion is
TWO-SIDED, since "inside the band" alone is satisfied by a unit quaternion
under which the test is vacuous — counter-factual: forcing k = 1.0 aborts it.

Mat4.fromTrs now declares the unit precondition its twins Mat3.fromQuat and
Quat.rotateVec3 both declare and it did not, with the consequence rather than
the bare word: the mix is anisotropic, so no scale factor describes it.

Reported and NOT fixed here, verified on the source in both directions:
body_manager.setRotation stores a rotation raw, reachable through the FROZEN
setBodyTransform and through sync_in.zig's per-tick ECS seam, while addBody
normalises and integration.zig skips exactly the .gameplay bodies syncIn
writes to. Forge, frozen surface, needs an M1.D number and an owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dedicated CI hotfix branch, before M1.2.1: it touches only ci.yml and is
validated by its own runs. The diagnosis, the four-point arbitration and the
two refuted hypotheses are recorded before any edit lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
THE CAUSE IS IN THE KEY. All three cache keys carry `${{ github.sha }}`, so no
save is ever overwritten: each commit creates two fresh archives per cell, and
restoration is necessarily a prefix match on the previous lineage. Growth is
monotone in the number of commits pushed — two entries x fifteen cells per run,
eight of them windows at 1-2 GB — against GitHub's repository-wide 10 GB
ceiling. The ceiling is not brushed by accident, it is reached BY CONSTRUCTION
in a handful of commits, after which LRU eviction takes over.

MEASURED on run 34777902366 at `5d79846`. Both f64 windows cells were CANCELLED
with every step green, `Complete job` included — 57 min against a 55 budget and
21 min against a 20 — so neither is M1.D.19 (which loses tests and prints `test
runner failed to respond`) nor M1.D.29 (an assertion genuinely falling). A
re-run at the SAME sha returned 9.6 min and 5.6 min, the only difference being
that the first attempt's save had run; the restore keys attribute it exactly,
`e60dda8...` then `4ad6487...`. And `ReleaseSafe / f32` took 52.1 min TWICE, its
own attempt-1 save having been evicted meanwhile by the other cells of the same
run. The cells evict each other INSIDE one run. Repository cache read live:
11.55 GB / 13 entries, then 9.44 GB / 11 a few minutes later, with the four
windows cells holding 8.44 GB of 9.44 — 89 % of the budget for 4 of 15 cells.

So this is not a slow CI, it is a NON-DETERMINISTIC one: the same sha passes or
fails on what eviction did meanwhile, and the re-run cleared it by luck.

POINT 1 — the `-build` save is deleted, with its now-dead measurement step
(exactly one consumer). Its unique window was a job killed between it and the
final save, i.e. the timeout, which saturation causes: it existed to survive an
ill it fed. This file already recorded it killing a cell outright —
`Save Zig cache (post-build)` measured going 39s -> 5m23s -> 7m39s on
windows-2025 / Debug while the useful work stayed flat, and that leg died at its
ceiling twice. The all-or-nothing doctrine was MOVED rather than deleted: it
lived in the removed block and the surviving block referenced it.

POINT 2 — 55 -> 75 and 20 -> 35, and the reason is not comfort. A budget's job
is to separate SLOW from HUNG, and at 55 against a cold cell measured at 52-57
it separated nothing: it turned a known slowness into a random failure and made
M1.D.19 indistinguishable from M1.D.27. The Debug budget was the tighter of the
two in proportion — 21 measured against 20 allocated — and no debt entry had
ever named it. The superseded "a cold Debug leg is ~11 min of work, ~45 % inside
the 20-min budget" is removed rather than left beside its correction.

TWO HYPOTHESES REFUTED, one per party. That the milestone had outgrown its
budget: the PREVIOUS run carried all three hotfix commits and both new tests and
did the same cell in 10.6 min — two tests do not cost 46 minutes. And mine, that
M1.2.0/G1 widened the blast radius by making `type_info.zig` import
`foundation`: measured, `command_buffer.zig` and `scheduler.zig` already did
before the milestone and `core_module.addImport("foundation")` dates from
M1.1.14, so breaking that one dependency would change nothing.

NOT IN THIS PUSH: saving only from `main`, whose cost is not known in advance
and which owes a before/after measurement on the same branch. And splitting the
cell by test domain is REFUSED for now — it is the only lever that aggravates
the root cause, since splitting multiplies cache entries.

Detail: `briefs/ci-hf-cache-saturation.md`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md's M1.1.1-HF3 row says the class was instrumented and that the first
recurrence would self-name via the dump. It did, on this branch's own CI, run
34795835386 attempt 1, windows-2025 / ReleaseSafe / f64 — and did not reproduce
at the same sha.

Written here because a scratchpad does not survive the session and this is the
only open PR; a repository file is the only durable place available. It is NOT
this hotfix's subject.

The dump establishes that the work was entirely drained (pending_count 0, the
13 chunks all consumed, so 2882 failed steals for 0 successes is correct), that
no worker entered a park (parks_entered 0 on all four) when all of them should
have, and that the test could not exit: its phase (a) loops until
sum(parks_entered) > sum(parks_completed), false forever at 0 > 0.

It does NOT establish why, and that is deliberately not guessed at. The question
it makes answerable by reading code: what does a worker do after an unsuccessful
steal, and under what condition does it enter a park. Tier 0, and its owner's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@guysenpai
guysenpai merged commit e9371a9 into main Sep 14, 2026
20 checks passed
@guysenpai
guysenpai deleted the ci/cache-saturation-hotfix branch September 14, 2026 07:16
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.

1 participant