Technical Documentation: ben-manes/caffeine
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (README.md)
Caffeine is a high performance, near optimal caching library. For more
details, see our user's guide and browse the API docs for the latest
release.
Cache
Caffeine provides an in-memory cache using a Google Guava inspired API. The improvements draw on our
experience designing Guava's cache and ConcurrentLinkedHashMap.
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(5))
.refreshAfterWrite(Duration.ofMinutes(1))
.build(key -> createExpensiveGraph(key));Features at a Glance
Caffeine provides flexible construction to create a cache with a combination of the following
optional features:
- automatic loading of entries into the cache, optionally asynchronously
- size-based eviction when a maximum is exceeded based on frequency and recency
- time-based expiration of entries, measured since last access or last write
- asynchronously refresh when the first stale request for an entry occurs
- keys automatically wrapped in weak references
- values automatically wrapped in weak or soft references
- notification of evicted (or otherwise removed) entries
- writes propagated to an external resource
- accumulation of cache access statistics
In addition, Caffeine offers the following extensions:
Use Caffeine in a community provided integration:
- Spring Cache: Simple, modern, productive Java
- Play Framework: High velocity web framework
- Micronaut: A modern, full-stack framework
- Coroutines: Kotlin Coroutines extension
- Bootique: A fast, simple Java platform
- Quarkus: Supersonic Subatomic Java
- Camel: Routing and mediation engine
- Scaffeine: Scala wrapper for Caffeine
- JHipster: Generate, develop, deploy
- Aedile: Kotlin wrapper for Caffeine
Powering infrastructure near you:
- Dropwizard: Ops-friendly, high-performance, RESTful APIs
- Cassandra: Manage massive amounts of data, fast
- Coherence: Mission critical in-memory data grid
- Accumulo: A sorted, distributed key/value store
- Kafka: A distributed event streaming platform
- HBase: A distributed, scalable, big data store
- Apache Solr: Blazingly fast enterprise search
- Infinispan: Distributed in-memory data grid
- Redisson: Ultra-fast in-memory data grid
- OpenWhisk: Serverless cloud platform
- Corfu: A cluster consistency platform
- Grails: Groovy-based web framework
- Finagle: Extensible RPC system
- Neo4j: Graphs for Everyone
- Druid: Real-time analytics
In the News
- An in-depth description of Caffeine's architecture.
- Design of a Modern Cache: part #1, part #2
(slides) at HighScalability - The Adaptive Window, From the Ground Up on optimizing shifting workloads
- Design of a Modern Cache: part #1, part #2
- Caffeine is presented as part of research papers evaluating its novel eviction policy.
- TinyLFU: A Highly Efficient Cache Admission Policy
by Gil Einziger, Roy Friedman, Ben Manes - Adaptive Software Cache Management
by Gil Einziger, Ohad Eytan, Roy Friedman, Ben Manes - Lightweight Robust Size Aware Cache Management
by Gil Einziger, Ohad Eytan, Roy Friedman, Ben Manes
- TinyLFU: A Highly Efficient Cache Admission Policy
Download
Download from Maven Central or depend via Gradle:
implementation("com.github.ben-manes.caffeine:caffeine:3.3.1")
// Optional extensions
implementation("com.github.ben-manes.caffeine:guava:3.3.1")
implementation("com.github.ben-manes.caffeine:jcache:3.3.1")For Java 11 or above, use 3.x otherwise use 2.x.
See the release notes for details of the changes.
Snapshots of the development version are available in
Sonatype's snapshots repository.
.Claude Docs Audit Output (`.claude/docs/audit-output.md`)
Audit Output Location
Write reports and transient analysis under .local/, grouped by producing model:
.local/audits/<model>/<name>.md<model>— the short id of the model that produced the report, lowercased, no vendor
prefix and no context-window suffix:opus-5,fable-5,sonnet-5,haiku-4.5,gpt-5.6-sol. When a run spans models (an auditor on one, an evaluator on another), use the
model that orchestrated it. An assignedAUDIT_REPORT_PATHoverrides this derivation
entirely; see Rules for agents.sharedoccupies the<model>slot for artifacts that are not one model's output: the
consolidated backlog, a bug spec worked across sessions, anything aggregating several runs.<name>— the invoking skill's name (audit-adversarial.md). A multi-agent run suffixes
per agent (audit-jcache-conformance-groupABC.md,audit-sibling-divergence-groupA.md) and
a verification pass adds-verification; the canonical unsuffixed name is the synthesis.
Never overwrite a report you were dispatched to verify, consolidate, or read.
There is no date or cycle level. Rerunning an audit under the same model replaces its report.
Examples:
.local/audits/opus-5/audit-jmm.md
.local/audits/gpt-5.6-sol/audit-jmm.md # same audit, other provider
.local/audits/fable-5/audit-adversarial.md
.local/audits/fable-5/audit-adversarial-evidence/r1-challenges.md
.local/audits/shared/audit-consolidated.md # cross-model queue index
.local/audits/shared/queue/<section>/<item-id>.md # one work itemKeep .local/ between sessions for handoffs and cross-model comparison. It is gitignored and
machine-local; checked-in guidance must remain useful if that tree is absent.
Consolidated queue
Cross-audit consolidation, including run-audits.sh --consolidate, uses this layout:
.local/audits/shared/audit-consolidated.md: brief workflow instructions, counts by status, and
section tables with linked ID, status, and short subject. Claims and evidence go in item files..local/audits/shared/queue/<section>/.md: one item per file. Sections, in order, arecore,async,jcache,guava,simulator,examples,build-ci,docs. ID prefixes match
the section exceptsimulatorusessim,examplesusesex, andbuild-ciusesci..local/audits/shared/queue/history.md: consolidation provenance, report inventory, coverage
limits, and prior cross-item decisions. Consult relevant portions, not the entire history.
Each item starts with # : <short subject>, Status: <status>, and Severity: <severity>.
Include a Prior rulings and context section linking to named anchors in history.md, then the
current decision and next step, original claim, source-report links and finding IDs, and earlier
evidence/counterarguments. Distinguish reported measurements from current checks.
Use open for pending work (including disputed claims), unverified for unresolved reachability,
contract, or impact, resolved for an evidenced repair, and closed for an agreed no-fix outcome.
Extend the queue in place. Reuse IDs for matching claims; assign new numeric IDs above the section's
maximum, never recycling or renumbering them. Separate distinct subclaims without dropping their
parent ID. Preserve resolved/closed decisions when claims recur, and retain contrary evidence.
Never prune items to meet a survival ratio.
Map history entries by mechanism and scope, including family members not named by ID. Label
near-matches and historical counterarguments without treating them as current dispositions.
Give each history entry a stable named anchor. When adding or revising one, update the affected
items' links. If a mechanism review finds no related entry, record that explicitly with the review
date; a missing section means unreviewed. Neither case exempts an item from canonical module rules.
After each pass, refresh index links and counts from item files, using an existing helper if
available. Verify report coverage, retained items/counterarguments, and relative links. Work from
the selected item and relevant rulings; use a fresh context for unrelated items and leave a concise
handoff. Per-skill synthesis reports remain in the producing model's directory.
Rules for agents
- The orchestrator computes the output path once and passes it to every agent as
AUDIT_REPORT_PATH. Use that path, not the agent's self-reported model name; group and
verification siblings go beside it..github/scripts/run-audits.shexports it;.claude/agents/auditor.mdPhase 4 and.claude/hooks/audit-report-guard.shhonor it. - The auditor agent must not read anything under
.local/audits/(prior conclusions bias a
fresh run) while still being required to write its own report there. See.claude/agents/auditor.mdEvidence Boundaries. - Long-running tooling (
audit-temporal-walk) takes its model from theAUDIT_MODEL
environment variable, since a shell-launched walk cannot know it; the invoking agent exports
its own model id.audit_paths.reports_dirfalls back to an existing tree for the module when
that variable is missing, so a resumed walk still finds itsstate.json.
.Claude Docs Audit Rounds (`.claude/docs/audit-rounds.md`)
Audit Rounds
Read when starting or triaging a batch /audit-* round.
Complete the audit batch before repairs. Use the
consolidated queue format, verify no claims or counterarguments
were dropped, then work one item at a time and revisit coverage affected by fixes. Keep the tracker
current across sessions. A full cycle has typically taken 2–3 weeks.
Serialize the runs. Quota exhaustion mid-run breaks an audit rather than pausing it, so
never fire runs in parallel. /audit-adversarial is the single priciest skill (roughly half a
weekly quota: 8 reviewers plus their evaluator challenges), so order the batch value-descending
and an early cutoff still banks the most bugs.
Prefer a second model on a skill over a third model repeating it. Prior paired runs produced
model-unique findings, including their most severe claims. The preferred order is:subsystem-safety, liveness, feature-interaction, jcache-conformance,sibling-divergence. build-ci and serialization are the least worth it.
Leave /audit-jdk-seam out of a batch. It is kept for an occasional manual run, not the
normal checklist.
Down-weight, but do not skip: re-entrancy (a full pass has closed at zero, and callback
re-entrancy warnings are not wanted, which removes the remedy from most of what it finds);
simulator periphery (a testing tool — its bugs mislead benchmarks, they do not harm users);
and the formal-shape lenses (jmm, linearizability, arithmetic, correctness-proof,
map-contract), which are cheap to run but have gone several passes without a core defect.
A report row is a claim, not a confirmed defect. An earlier sweep reported 186 claims,
26 after source review, and 7 after standing rulings; even high-rated claims were rejected.
Those historical counts do not establish a rejection target or release confidence. Check the
contract and matching module rulings first. Use a focused reproduction for a behavior repair;
a source/contract proof can settle a documentation correction or no-code disposition. Preserve
the trigger, controls, counterarguments, and evidence limits.
Use general-purpose, not auditor, for verification, triage, and consolidation passes
over existing reports. The auditor carries a mandatory-report-write gate (SubagentStop) that
will pick a canonical filename and overwrite the source report you are consolidating from. If
you must use it for reuse, assign it an explicit -verification path.
.Claude Docs Design Decisions (`.claude/docs/design-decisions.md`)
Design Decisions
Intentional behavior and review invariants, with their rationale and evidence. Read the
relevant section before changing a mechanism; use ruled-out for adjudications.
Eviction
Weight=0 is a pinning feature. Entries with weight=0 are skipped during eviction
(evictFromWindow, evictFromMain, evictEntry). This is an intentional user-facing
API (inherited from ConcurrentLinkedHashMap where weight was >=1, Guava's Cache where >= 0).
Used internally for in-flight async futures.
The re-scan cost is accepted. evictFromWindow restarts at the deque head each cycle and
never relocates a zero-weight node, and evictFromMain skips its victim inline, so cold
pinned entries at the LRU end are re-traversed whenever the region is over budget — O(pinned)
per cycle in the worst case. A dedicated zero-weight queue to hold them out of the scan was
explored and rejected: it made the eviction paths messier for little value. Don't propose it
again without a measurement showing the scan actually costs something.
Region transfers are budgeted per maintenance cycle and re-armed. evictFromWindow anddemoteFromMainProtected move at most QUEUE_TRANSFER_THRESHOLD entries per cycle and setPROCESSING_TO_REQUIRED when a backlog remains, because a setMaximum shrink can leave either
region arbitrarily oversized and one cycle would otherwise drain it whole under evictionLock.
The climber's increaseWindow / decreaseWindow carry their remainder in adjustment instead
(see the climber entries below). A region briefly over its maximum right after a resize is the
budget, not a leak; the protected demotion lacked the re-arm from the 2019 adaptive commit until
the 2026-08 audit sweep, which is why an idle cache could sit with protected oversized until the
next access. Don't remove either re-arm.
Transient negative weightedSize is acceptable. maximumSize allows eviction
before/after threshold. Eventual consistency is fine given documented promises.
Weight convergence is guaranteed by the telescoping sum property across all write
buffer task orderings.
Weights are static after computation. The weigher is called at creation/update
time. The weight is not recalculated afterward — relative weights don't influence
eviction ordering, only total capacity accounting.
FrequencySketch.reset()'s pause is accepted, and chunking it is the wrong trade. The reset
halves every counter under evictionLock: 0.29 ms at a 100k maximum, 2.0 ms at 1M, 32 ms at 10M,
238 ms at 100M. It is rare in proportion to the cache, since sampleSize is 10 × maximum and the
reset fires when that many increments have been recorded, so a 100M cache resets once per billion
recorded reads, about 0.24 ns/read amortised. Amortising the pause means chunking it, which lowers
eviction quality by querying a half-reset sketch. The default async eviction hides the pause
entirely, and on a caller-runs executor the worst case is still cheaper than the misses the sketch
prevents. The count += Long.bitCount(table[i] & ONE_MASK) odd-counter correction is a
cross-iteration reduction and is what stops the loop auto-vectorizing; dropping it is much faster
but loses the correction, and no argument has justified that. SIMD is the answer once it is
available. It has produced no user reports and does not show in GetPutBenchmark.
A weak-key lookup allocates, and that is accepted. The weak-key node factories build aLookupKeyReference per lookup (20.0 ns/op and 24 B/op against 2.6 ns/op and 0 B/op for strong
keys); it is handed to ConcurrentHashMap.get, so escape analysis cannot remove it. Avoiding the
allocation means caching one wrapper in a thread-local and mutating it to set and unset the
referent around each lookup, which pins that instance to the thread. That idea was raised and
rejected in issue #294 over virtual threads, where an instance per virtual thread is unbounded,
and over classloader pinning. A young-generation allocation is the accepted trade.
Policy weight uses a signed 62-bit payload returned as a long. Reordered update tasks
are normal: the value is written under the node monitor but the task is queued after it is
released, so a second writer can queue first. The node's own field then walks past the true value
and back, which the design tolerates. What it cannot tolerate is truncation. Once the intermediate
sum leaves the int range, a transfer copies the truncated value into the long region totals, and
nothing replays that copy: the node settles at its real weight while windowWeightedSize ormainProtectedWeightedSize keeps a permanent 2^32 residue. Measured 2026-09-08 with weights nearInteger.MAX_VALUE, the window then reads as permanently over its maximum, evictFromWindow
drains it every cycle, and every later arrival goes straight to probation, so the admission window
is dead for the life of the cache. The released 3.2.4 and 3.2.5 reproduce it, so it is not new.
A sign test on the transfer does not close it: half the wraps land positive, where the value is a
plausible weight. The write buffer bounds queued contributions, but not writers paused before
publishing their updates, including inside inline removal listeners. The historical witness
reached 34 bits; no practically reachable overflow of the packed payload has been demonstrated.policyWeight holds the low 32 bits and metadata's spare 30 bits the signed high portion, at no
extra layout cost (JOL over all 147 node classes is unchanged, where a plain long field grows the
16 strong-value weighted classes by 8 bytes). Pinned byBoundedLocalCacheTest.put_reorderedUpdates_leaveNoRegionResidue, which orders two writers through
a removal listener on a direct executor. Don't re-narrow the field, and don't guard the transfer
on the sign instead.
Two notions of "weighted", and the internal one gates on both. The isWeighted field is whether
the caller configured a weigher, and it is what Policy.Eviction reads for isWeighted(),weightOf and weightedSize (CaffeineTest.maximumWeight_large pins this for an explicitWeigher.singletonWeigher(), where the method answers false). BoundedLocalCache.isWeighted()
answers a different question, whether entries may be assigned different weights, and that is what
decides whether the frequency sketch can be sized from the maximum or has to be sized from the live
entry count. Neither test alone answers it: an async cache always wraps its weigher inAsyncWeigher, so identity against the singleton weigher says "varies" even for an unweighted one,
while a caller who passes Weigher.singletonWeigher() explicitly sets the field even though every
entry weighs the same. The method conjoins them. Sizing the sketch from the entry count is not free:ensureCapacity forgets all counts when it grows, so filling a maximumSize(10_000) async cache
allocated the sketch twice (8192 then 16384) and discarded the frequencies gathered over the first
half, where the sync equivalent allocates 16384 once. It also called data.mappingCount() on every
insertion past half the maximum. The one case still answered wrongly is an async cache given an
explicit Weigher.singletonWeigher(), which needs unwrapping AsyncWeigher to reach and costs only
that same extra warmup allocation. BoundedLocalCacheTest.isWeighted_onlyWhenWeightsVary pins the
cases answered correctly.
The climber has three size tiers, in the configured maximum's native units. Weight units
are deliberate: the weighted stress track included about 200 entries of 25–100MB in a 10GB
cache and scored 14 wins / no losses. A later comparison on 18 byte-weighted trace cells found
no consistent reactive advantage at low resident counts. Grow-only entry-count selection gave
small, mixed changes; bidirectional selection repeatedly reset the controller on cells crossing
the threshold. Retain the configured-weight threshold on this evidence; see
entry-count tier selection.
Many tiny entries under a small weight bound land on reactive, which remains safe there.
| Maximum | Controller | Reason |
|---|---|---|
| ≤512 | Slow reactive: grow first, minimum initial step 2, decay 0.995 rather than 0.98, period stretch capped at 4× | Integer windows and noisy samples make an initial shrink a no-op and prevent later reversal. |
| 513–4096 | Standard reactive | Density was near-neutral below ~2048 (+0.12–0.24pp), but lost up to 10pp on corda+loop at 513–1024. |
| >4096 | Goal-audited density | Added about 125pp across the large cells of the 48-trace set while accepting small frequency-trace regressions. |
The gates are SLOW_ADAPT_THRESHOLD and DENSITY_THRESHOLD; slow tuning usesSLOW_ADAPT_DECAY_RATE and SLOW_ADAPT_RATIO_CAP.
Density compares regions within one sample. It usesln((windowHits / windowMaximum) / (mainHits / (maximum - windowMaximum))), with a step
proportional to error (DENSITY_GAIN = 0.03), capped at MAX_STEP_FRACTION (30% of maximum).
This avoids the cross-sample workload swings
that obscure reactive gradients on a flat hit-rate curve, but it is average resident density,
not marginal value. Its equilibrium depends on the starting window; ghosts would supply a
different signal but were rejected for cost. Extending density below 4096, even with the probe
machine, lost 1.3pp on cs@563. Kickoff, regret, anneal, and wide-start variants traded one
small-cache trap for another rather than removing the bias.
Blind corners require experiments. A region with fewer than requestCount >> MIN_SIGNAL_SHIFT hits (~0.1%) supplies too little evidence to hold position. Pure density
can pin at the floor about 28pp below LRU at any size. Probe only when the starved region is
small (≤¼ maximum), or the whole sample is dead; probing for a starved large main region
destroys the functioning window on corda. Deep refractory rungs increase both reach and
commitment, while cheap early adjudication protects thin-signal floors. Failures command a full
return and back off (PROBE_BACKOFF_INITIAL 16 through 64 samples), costing about 1pp on short
w50/S1 traces and amortizing toward
zero with longer runs. Preserve the current router and ownership rules in
climber review constraints and hill-climber §4.
The evidence behind the asymmetric verdicts and crash prices remains relevant:
- Starvation-walk interior noise pricing is
min(max(5pp, 3×rateDeviationEma), 15pp).
It recovered 8.2pp on the dosed-mixture trap; dropping the cap lost 0.6pp onmetronome. - Audit crashes retain the depth bound and buy persistence in time. The first crash is cheap;
an already-crashed equilibrium's retry tolerates two below-bar samples and aborts on the
third. This raised the constructed moat from 41.97 to 44.3; a 22-cell real-corpus scan found
no echo. Giving every walk tolerance failedmixture_d025/mixmodon short traces. - Sharing crash state paired independent pulses into rung 64 / clock wait 128 and a
130-sample floor pin, 6pp below LRU. Non-crash endings retire only their owner's streak;
an audit confirm alone also clears starvation state when resetting that ladder. Pins:audit_budgetExpiry_leavesTheStarvationLedgerandwalkStep_reversalThroughBase_leavesTheOtherLayersLedger. - An up-probe prices the capacity it takes from probation, frozen at arm. Main's protected
average vetoed useful positions ontrickle(14–17pp below its own engine); live probation
became enriched by the walk's demotions and vetoed every escape ondemoflood. Down-probes
keep the average comparison. The low-hit-ratelowmixtrade remains a gate sentinel, with
no real-trace echo across the defended corpus. - Crash veto precedes adjudication, so a destroyed region cannot win by a density ratio
against another region earning nothing. A neutral verdict is not a success (S3). The
current reversed-confirm rule is in the checklist below; older blanket reward rules no
longer describe the machine.
The density floor is 2% (WINDOW_FLOOR_FRACTION), with an upward clamp from below it.
The initial window is 1%, so
a clamp that only blocks further shrink can wedge below the intended floor. At size 600 a
0.5% floor held only three entries and the recency phase scored about 23% versus 44% optimum.
The 2% large-cache safeguard cost at most 0.35pp on frequency-optimal workloads; kickoff and
EWMA-regret escapes lost up to 8pp on w50 or only moved transiently before retrapping.
Re-run corda+loop at 4097–8192 before lowering the floor. Resizing resets probe state andsample.previousHitRate, so the new geometry is not judged against an old sample.
The sketch's shrink retrack and reset()'s zero clamp are a matched pair. ensureCapacity
keeps the table (it is grow-only, since reallocating wipes the counts and blacks out admission)
but re-points sampleSize at the new maximum, so a shrunken cache does not age on its old size's
cadence; the observed count is clamped to just below the sample to keep the equality reset test
reachable. That retrack breaks the precondition reset() was written under: count/4 < size held
only while the table matched the sample (count/4 ≤ 8·maximum < 10·maximum = sampleSize), so
without a floor at zero the correction underflows to a large negative size and no counter is
halved until ++size climbs back — restoring exactly the old-size cadence the retrack removes,
and doing it for roughly 4 × tableLength additions across successive resets. Measured
(2026-08-07, audit-arithmetic F1): a 64K-entry cache shrunk to 256 lands at size = −62,579,
about 25 sample periods, and recovery is driven only by novel traffic — 1M requests against
saturated counters advanced it by 932. Reachable with no Policy call: a weighted cache retracks
on every addition, so updating resident entries to heavy weights collapses the entry count
(200,000 → 20 measured) and drives it. During the stall admit() degrades to tie-breaking, so a
post-shrink working set cannot displace stale survivors. The clamp is inert whenever the table
matches the maximum, so it costs nothing on the normal path. Don't remove either half. Pinned byFrequencySketchTest.ensureCapacity_shrink_denseTable_agesOnSchedule — note its neighbour_shrink_resetReachable exercises the same flow on an empty table, where the correction is 1,
which is why the underflow shipped.
The retained table's reset cost after a large shrink is accepted. After a 1M-to-1K
shrink, an 8 MiB table took 1.94 ms under evictionLock, about 194 ns per increment amortized
against 0.19 ns when table and maximum match. Cost scales with peak/current ratio: a 10× swing
was negligible, while ~1000× was visible. This is reachable through unweightedPolicy.Eviction.setMaximum as well as weighted retracking. Hysteretic shrinking was declined
because it conflicts with per-addition weighted retracking and loses the admission history.
The large-cache sample period is SAMPLE_MULTIPLIER × maximum (4×), decoupled from the
frequency sketch's own 10× reset (they use separate counters — sample.hits/sample.misses
gate
the climber, FrequencySketch.size gates the sketch reset). At 10× a large cache gets only one to
three adaptations over a finite benchmark trace and never reaches a large (20–50%) optimal window; 4×
gives it enough steps to converge. It is kept at 4× rather than lower because a shorter period makes
the density estimate near a small converged window noisy enough to jitter a frequency-friendly
workload off its optimum — the cs trace craters at 2×. The density signal is inherently
recency-biased (it over-values the window), so frequency-optimal traces (fiu_madmax, cs,fiu_webmail, several ARC traces) give back ~0.5–2pp versus the reactive climber; on every one they
still beat LRU and match/beat Merlin and stay near-optimal, and the trade buys large recency-workload
gains (corda's cliff, OLTP/fiu_ikki/metaCDN/fiu_homes converging to their ceiling). An
adaptive period (short while moving, long once settled) was tried and rejected: starting short
made dup-heavy/phasey traces jitter persistently and never lengthen, cratering cs worse than a fixed
2×. Don't reintroduce it.
The climber is held until the cache is half full, and the gate is residency, not the sketch's
existence. climb() skips determineAdjustment while weightedSize() < (maximum() >>> 1), discarding the
sample through WindowClimber.discardSample once it fills, and still resets it outright when the
sketch is uninitialized. A cache below that
line is not evicting, so its sampled hit rate says nothing about the window/main split, and the
sketch's sampleSize caps the sample period, so a sketch sized for less than the maximum reads
that meaningless signal on a compressed cadence. Caffeine.initialCapacity(n) is one way in (the
generated constructor calls ensureCapacity(min(maximum, n))) and growing maximum throughPolicy.Eviction.setMaximum is the other. Measured 2026-09-08 on product.Caffeine: amaximumSize(400_000) cache hinted at initialCapacity(1000) scored 29.27 on arc/S3 against
41.04 un-hinted, arc/DS1@1M 8.76 against 12.48, arc/P3@100k 33.50 against 40.43, and the
window sat at 47–80% of the maximum forty million operations later where the un-hinted arm rested
at 26%. The sketch reinflates on schedule at half fill; the window it drove to does not come back,
which is why the old acceptance ("a transient that self-heals as the cache fills") was wrong. Don't
weaken the gate back to the sketch's initialization alone, and don't latch it. The unlatched form
is what closes the setMaximum growth case, and it is two field reads under evictionLock. Pinned
by BoundedLocalCacheTest.adapt_partiallyFilled_holdsWindow / adapt_filled_climbsWindow.
Climber adjustment is a multi-cycle carry-over, not stale state. increaseWindow /decreaseWindow transfer at most QUEUE_TRANSFER_THRESHOLD (e.g. 1000) nodes per maintenance
cycle, then store the unfulfilled remainder back into the climber's adjustment
(quota / -quota) — the leftover, not zero. On a large cache the per-decision step
(≈ 0.0625 × maximum, e.g. 62,500 at a 1M maximum) dwarfs the (e.g 1000) node cap, so a single
climber decision is deliberately drained across many later cycles. Each of those cyclesdetermineAdjustment early-returns at requestCount < effectiveSampleSize (the sample was
reset and has not refilled) without touching adjustment, and climb re-applies the
carried remainder. Once the sample refills, a fresh determineAdjustment overwritesadjustment with a new decision. This looks like "a stale adjustment re-applied without a
fresh hit-rate sample," but it is the completion mechanism for a work-capped transfer.
The symmetric give-back after the transfer loop (mainProtectedMaximum += quota; windowMaximum -= quota) keeps the partition sum (windowMaximum + mainProtectedMaximum + implicit-probation == maximum) constant — the maxima track the partial transfer that
actually happened (added by "Fix bugs in adaptive policy"). Three consequences
worth not flagging:
- Pinned leftover. If the carried
quotais smaller than the policy weight of every
candidate (e.g.quota = 1while all entries weigh 100), the loop moves nothing and
re-stores the same value, so the window stays put until a real sample overwritesadjustment. The window genuinely cannot grow by a fraction of an indivisible heavy entry.increaseWindowmay already have demoted protected entries against its provisional cap.
Giving back the quota restores the caps, not those entries' positions; later promotion depends
on accesses and available protected capacity. - Probation is the implicit slack region. The transfer draws from both probation and
protected but only decrementsmainProtectedWeightedSizefor protected moves, so probation
absorbs the difference between the window-maximum shift and the protected weight moved
(Δ windowMaximum == total weight transferredholds exactly). - The quota is a soft knob, not an accounting invariant.
quotais how much room the
climber may borrow from the other region for a probabilistic guess about an unknown future;
taking a little too much or too little is meaningless. A node carrying a transient negativepolicyWeight(a same-keyUpdateTaskreordered against its predecessor — see "Two weight
fields") passesquota < weightand inflates the quota viaquota -= weight, so the
give-back can overshoot and movewindowMaximum/mainProtectedMaximumopposite to the
commanded direction, or out of[0, maximum]. That is not a defect and must not be clamped:
the partition sum still holds, the region weighted sizes stay exact (they debit the same
snapshot they credited), a negativewindowMaximum/mainProtectedMaximumonly makesevictFromWindow/demoteFromMainProtecteddrain that region, a policy-quality wobble. An
out-of-range cap walks back only by the weight each later transfer moves: themin(adjustment, donor),<= 1andmax(0, …)guards stop a call from pushing it further
out, they do not pull it back. A swing larger than a cycle's transfer (one key's weight
swinging by more than the window holds) therefore suspends the split for many cycles, the
window cap abovemaximumidlingevictFromWindowwhileevictFromMainstill bounds the
total; the verdict does not turn on the duration. The invariant that matters is
thatpolicyWeightconverges, so a region's size keeps reflecting the entries inside it;
how a mid-flight snapshot lands on the quota does not. Clamping the quota also would not
restore a reservation — it just relocates the inaccuracy from the maxima to the transfer
volume. The duration of the excursion does not change this ruling.
The hardening companion to this: ReactiveClimber.samplePeriod guards the small-cacheratio against a 0/0 NaN (when both the maximum and step size are zero). The NaN would
otherwise zero the effective sample size, defeat the sample guard, and poisonsample.previousHitRate. Neither construction nor reactive decay reaches that state.
Generated constructors call WindowClimber.resized(maximum()), which seeds a step of at least2.0, including for maximumSize(0); decay never rounds it to exactly 0.0. Keep the guard
as defensive hardening. BoundedLocalCacheTest.adapt_smallCache_zeroMagnitudeDoesNotPoisonHitRate
explicitly forces the zero step.
The climber commands in double; the cache applies (long) of the command, truncated
toward zero once at publication. Positions, region maxima, and the walk's base are long, and
position identity is band-based (Reading.stableBand, 2% of the maximum), so a command's lost
fraction is not itself an error. Two consequences follow and are accepted: walkStep's
base-crossing predicate reads the continuous command, so a reversal whose truncated landing is
exactly the base fails the walk one sample earlier than an integer predicate would (the same
ending, one sample cheaper); and the 2% floor is a double, so the integer window rests one entry
below it (pinned by walkStep_floorBasedWalk_endsAtBudgetWithAFullUndo), and at maxima above
2^53 weight units the floor comparison has a rounding band of a few units, which is unreachable.
Don't make the floor or the predicate integral.
Undo accounting closes in integral published commands:undoRemaining is a long charged with each return command as published, not with the
fractional capped stride. Charged with the fraction it closed short of the base by the cap's
fraction per capped stride (8,192: 2,457 + 2,457 + 84 for a 5,000 return), and at a permanently
starved corner, where every deep-rung probe fails and undoes, that re-based each cycle 1–2 entries
toward the probed direction, a slow creep toward the corner boundary. Pinned byprobeEnding_adjudication_wrongSignFailsAndDoubles (the commands sum to the distance).
Physical undo arrival is best effort. The cache can retain unapplied movement when an entry
does not fit or the transfer budget runs out; the next completed sample replaces that carry.
Several capped strides can each lose a remainder, and later probes can inherit the resulting
landing. There is no one-entry bound per return or across returns, and the transfer-cap case also
affects unweighted caches. Retaining the base and reconciling actual movement was measured in two
forms: bounded to the original return duration, or continued until arrival/nonprogress. Across ten
weighted cells at eight seeds, mean object-hit-rate changes were only -0.050 to +0.060pp, while the
latter prolonged retreats and showed a throughput slowdown signal. Retain the approximation on
that evidence, not an assumption that displacement is harmless or confined to tiny caches. See
reconciling failed-probe returns for scope and costs.
Async load completions replace quietly. A completed future's handleCompletion (and the
bulk fillProxies) calls replace(..., quietly= true): the UpdateTask finalizes the weight and
expiration but skips onAccess's sketch increment and climber hit counters. The entry already
paid its miss at insertion; counting the completion as an access doubled the key's per-load
admission frequency and window-attributed one synthetic, write-buffer-lossless hit per miss —
measured at up to −38.6pp (w50) on the density climber and −12.7pp (corda+loop stress @ 512) on
the reactive climber. A
material quiet update (weight changed, or the write time moved beyond the 1s tolerance) routes
through the UpdateTask and still reorders the deques; an immaterial one skips policy work.
With access expiry alone, a slow same-weight completion can leave a live head ahead of older
expired entries when the tail is newer. Cleanup can defer reaping those entries until the head
is reordered or expires; reads still filter them. This is the same accepted delayed reaping as
when a read-buffer reorder is dropped. User-initiated writes remain loud by design. Don't re-add
access recording to the completion path, and don't flag the immaterial-completion skip as a
missing reorder/refresh. Pinned byExpireAfterAccessTest.quietCompletion_staleOrder_defersPhysicalExpiration.
The refreshIfNeeded completion's remap is quiet the same way (RemapHints.quietly, honored atremap's update dispatch): the triggering read already recorded its access, so a loud reload
completion double-counted every refresh-eligible read into the sketch and the climber's hit
sample (1000 reads → 2000 of each once past the refresh interval). A reload finalization is
bookkeeping, not a usage. Manual LoadingCache.refresh / async tryComputeRefresh completions
are unchanged (explicit per-call API action, no read-stream amplification); revisit only with a
measured skew. Pinned by BoundedLocalCacheTest.refreshCompletion_doesNotRecordAccess.
A committed refresh completion always takes the material branch, so remap's immaterial one is
quiet-capable but unreached in production (a rejected one returns at the preserveTimestamps
exit, before the dispatch). refreshIfNeeded requires refreshAfterWrite, which
makes exceedsWriteTimeTolerance's refresh disjunct true at every completion: either the
configured duration is within the 1s tolerance, or the refresh fired because the entry aged past a
duration longer than the tolerance. The quietly guard on that branch is not defense-in-depth, it
is the same contract replace implements for the async completions, whose callers do not requirerefreshAfterWrite and do settle immaterially. Don't delete it as dead code; it is pinned through
the compute(..., hints) seam by BoundedLocalCacheTest.remap_quietly_doesNotRecordAccess and its
loud twin.
In-place reloads record a climber miss. computeIfAbsent, compute, put, and putIfAbsent
reuse a node when its entry has expired or its value was collected. Their UpdateTask usesAccess.RELOAD to increment the sketch and record the miss an AddTask would record after
physical removal. replace rejects dead entries. Access.QUIET takes precedence for internal
refresh completions.
For caches with size eviction, a nonquiet reload must use UpdateTask even when the weight is
unchanged and timestamps are within tolerance: the read buffer always records Access.HIT.
Access expiration without write expiration exposed this missing case in remap;expiredRemap_recordsClimberMiss covers both expiration modes.
Steady contamination can cancel in cross-sample rate differences, but it biases the density
tier's within-sample regional ratio. In an earlier synthetic reuse study (maximum 512, reuse
gap equal to expiry), misattribution moved the converged window from 402 entries to 5 across
all seeds and both maintenance lags. Nearly all false hits were in main, and reloads rose from
4.2% to 13.6%. Those measurements concern the earlier reload-accounting fix, not the
same-weight remap case. No end-user hit-rate loss has been established for either case.
The simulator's product.Caffeine uses neither expiration nor reference values, so its gate
cannot exercise this path. Pins: BoundedLocalCacheTest.expiredReload_recordsClimberMiss,expiredRemap_recordsClimberMiss, expiredPut_recordsClimberMiss, and put_recordsClimberHit.
They also check the sketch increment, which a quiet update would incorrectly suppress.
Random admission of rejected candidates. The TinyLFU admission filter admits 1 in 128
candidates of frequency 6 or more that would otherwise be rejected. This is the HashDoS
protection: an entry whose frequency hash collisions have raised, whether it is the victim or was
admitted as an inflated candidate, is eventually evicted rather than holding its place. It lets the
inflated entry leave the cache; it does not prevent its admission (see
ruled-out, Eviction and maintenance).
Climber review constraints
The current machine and its rejected alternatives are described in hill-climber
§4–6. Preserve these distinctions when changing related branches:
- Recovery takes samples. A density sample is 4× maximum requests; typical log error of
1.5–2 yields steps of 4–6% of maximum. Descending from an 80% window takes 13–16 samples,
or 52–63× maximum requests. On ten frequency-optimal cells, 41 samples recovered 73% of
an 80% plant, two samples recovered 2%, and 4× replay cut the deficit 3–8×. Density recovered
roughly twice as much as reactive. Shortening the period needs a measurement of its jitter cost. - Reactive reversal substitutes for a floor. The reactive tier can reach a one-entry
window;Reading.floor's 2% floor belongs only to density. A banded reactive law without a
floor drove corda from 30.96 to 1.13. Any change that sustains its runs must revisit the floor. - Setpoint density has an accepted burst bias. Budgeted transfers can leave actual window
occupancy abovewindowMaximum, inflating its density. Candidates still passadmitandLocalCacheSubjectchecks the bound at quiescence. Do not substitute occupancy without measuring. - References differ by purpose. Up-probes freeze probation density and its sample length
at arm, scaling the baseline to the live sample length; down-probes compare one sample.
Starvation-walk deviation pricing stays live. Audits freeze their rate reference and use a
raw-sample streak plus one beat-base test. The rail's3×deviationprice is separate. - Each layer owns its state. Crash streaks and ladders belong to their walk. Non-crash
endings clear only that layer's streak, so alternating crash/budget endings do not repeatedly
get tolerance. Crashes never double the audit clock. Stillness alone advances that clock;
moving samples decrement its run by one rather than reset it. Only interior-chosen directions
update the alternation bit; a due audit pre-empts a refractory hold. - Walk exits use different bars. Audit crash abort is a level test against the arm's frozen
rate, with 5pp depth capped atAUDIT_BAR_FRACTIONof that rate. Reversal is a first-difference
test, priced byAUDIT_BAR_FRACTION × max(baseHitRate, noiseBand)under the same absolute cap.
That fraction is0.15 × VETO_MARGIN_SCALE; widening the bars failed the measured controls.
A floor-based walk can misscrossesBasethrough integer rounding; budget expiry gives the
same FAILED price and full return command, pinned byWindowClimberTest.walkStep_floorBasedWalk_endsAtBudgetWithAFullUndo. - Anchor discard and metric reset stay paired on crash-scale shifts. The inherited EMA is
about 80% old regime. A distant crash keeps the claim and reference. On-anchorresyncruns
even during return drain; planting waits for both a walk and its pending undo to finish.
Symmetric claim aging lost 11.9pp on the ramp control; one-sided aging disarmed the rail.
The pairing also holds inside a starvation walk, whose crash and reversal bars the reset widens
to their cap: a census of the battery found seven such stand-downs, all on gains, which neither
bar judges, and skipping the reset through 38 of them on seeded runs changed no hit rate. - An audit judges against the position it leaves.
Walk.baseSmoothedRate, notAnchor.rate,
is its baseline (ghostclaim31.2 → 48.5;cp_w100+2.0). Discarding a claim on a still swing
loses retreat recovery (moat_h3000, −0.7 to −1.9 on all eight seeds); resetting the metric
while keeping the claim sends the rail to a dead anchor (ghostclaim_p3041.2 vs 49.2). - A return retests its frozen claim only at its original position.
RETEST_SETTLE = 2
avoided the seed losses of 1 and 3;ghostclaim_p35/p40improved 31.7 → 33.9 and 31.4 → 33.5.
Retest followsanchor.returning, rechecksisAt, and is cleared by bothdiscardandplant.
Clearing only on stand-down misses budget-expired returns and confirmed-position replants
(#2002). Do not retest a claim after its anchor has moved onto a position the return never reached. - A reversed starvation confirm is a completed experiment. Escalate its ladder and hand
to density with zero refractory; rewarding it restarted a recurring dither (668/881 confirms,bandtrap2−4.4pp and an absorbingshallowmoat). Accepted costs includearc_DS1−0.7,deadphase−0.2, andnorank_rep_r6seed 3 falling 41 → 20 while seven seeds were unchanged.
The unlandedwedgeshiftguard needs a holdout. Only deepest-commitment, audit-grade confirms
park; other starvation confirms keep their density handoff. - Park and refractory scopes remain narrow. A parked audit covers its own walk's crash-scale
move, but an external shift and undo arrival still judge the park (demoflood−1.9 if widened).
Only a starvation undo arms the starvation refractory. Removing audit-undo rearming boughtwidepin+5.1,rep_r6seed 3 +5.4,shallowmoat+1.1 formetronome−0.9,balloonflip−0.3 andcp_w050−0.55. - Repeat confirms deepen rather than reward. Per-direction memory tracks the farthest
confirmed window; starvation failure/crash clears it, audit endings and anchor discard do not
(absolve_p827.95 → 46.3). A park's first audit follows its confirming direction only while
the park and smoothed-rate guard still hold. Consume that exception at arm; later audits
alternate. Raw-rate substitution failsabsolve_p12; unguarded direction costsmoat_h5000
1.75pp and failsclimbtrend_up/whisper_mod_p6. - Main-space experiments already priced the alternatives. Across 276 cells, plain-LRU main
lost 93.1pp net (mean −0.337); SLRU won >1pp on 40 cells versus nine, 38/46 traces preferred it,
and N=3 had no sign flips. Partial promotion gates were worse than either extreme (−25.7/
−20.3pp tails), because sparse protected entries stay outside probation's victim pool.
The 80/20 split and one-hit promotion beat alternative constants; a perfect per-cell oracle
was only +0.21pp after the max-of-N noise floor, with winners changing across sizes on all
46 traces. Wrong constants cost −6.16pp to −25.72pp. Probation stays near 19.8%; the density
window borrows protected capacity over roughly [2%, 80.2%].
Audit confirms park without a parting steering step; starvation confirms normally do neither.
The room check and walk use Ladder.stride. Before changing floor-crossing arithmetic, use
seeded admission comparisons to distinguish a repair from a shifted probabilistic outcome.
Expiration
EXPIRE_TOLERANCE = 1 second. Expiration is a maximum lifetime,
not a minimum hold time. Like ScheduledExecutorService, the timing is never exact.
The tolerance applies to multiple per-entry timestamps:
writeTimereorder decisions when a write updates a live value
(exceedsWriteTimeTolerance) — avoids write buffer saturation from rapid timer wheel
rescheduling, ~4x throughput on write-heavy workloads. A write over an expired or collected
value is a creation and restarts the write time.accessTimeupdates on the read path — avoids cache-line true-sharing on a hot
entry underexpireAfterAccess. When the configured duration is<= tolerance
the skip is bypassed so tiny expiration windows still behave exactly.variableTimeupdates on the read path — the same saving under anExpiry, with the same
bypass. Only an extension is skipped: a read that moves the deadline earlier is stored, since
skipping it would serve the entry past the deadline theExpiryasked for. An unchanged
deadline is not rewritten, even inside the bypass. UnderExpiry.creatingorwritingevery hit
in an entry's last second keeps its deadline, and rewriting it was a CAS that cost ~45 ns per hit
on 8 threads. The skip is pinned byBoundedLocalCacheTest.expireTolerance_variableTime_skipsForUnchangedShortDuration.
A put dates a new entry from its call, not its insertion. The create path reads the clock
when it builds the node, before data.putIfAbsent, and a retry after a node-monitor wait reuses
that node. An insertion that waits on another thread's bin (a same-key computation that returns
null, a bin neighbour's computation, a synchronous eviction listener in maintenance) publishes the
entry already aged by the wait, so it expires early by that much, or is a miss on the next read
and an EXPIRED notification when the wait outlasts its duration. That is not a defect: the
contract does not say when the clock starts, and either placement satisfies it. The caller obtained
the value before the call, so after an hour's wait the value is an hour older, and dating it from
the call does not overstate its freshness. A computation dates its result after the function
instead (#191), because the cache produced that value under the lock. The price of a back-dated
insert is in the order deques: appended at the tail with the oldest time, it makes the nextexpireAfterWriteEntries scan relink every fresher live head before reaching it (one walk of the
live deque, once per such insert), and in access order it can linger unreadable behind live
heads until they expire, with size eviction reporting it as EXPIRED.
ASYNC_EXPIRY = ~220 years (Async.java). Computing futures get this sentinel
duration to prevent expiration during async computation. The isComputingAsync()
check tests both the isAsync flag AND whether the future is complete.
The sentinel also records that a load has not been accounted for, so every
read-extension path preserves it. AsyncExpiry.expireAfterUpdate routes to the
user's expireAfterCreate when currentDuration > MAXIMUM_EXPIRY, which is howhandleCompletion's quiet replace tells a first load from an update. AsyncExpiry
cannot make that distinction on a read: it keys off getIfReady, so once the value
arrives it delegates and returns a real duration. The guard therefore belongs in the
cache, and both tryExpireAfterRead and expireAfterRead carry it (isAsync && currentDuration > MAXIMUM_EXPIRY returns without consulting the Expiry). The
window it covers is not the in-flight state, which AsyncExpiry handles: it is the
value arriving while the completion waits on the node lock a putIfAbsent is
holding. Consuming the sentinel there also hands the user's callback a 220-yearcurrentDuration, and an Expiry that returns what it was given, Expiry.creating
among them, then pins the entry for that span instead of expiring it after its
configured duration.
The completion finalizes only what the insertion deferred. handleCompletion
finalizes an entry with a same-instance quiet replace, which lands on the update path.
For a future that was in flight when it was inserted that is the entry's first and only
evaluation, since the install stored the sentinel and AsyncExpiry routes it back toexpireAfterCreate. This includes an incomplete future replacing a ready value: it begins a
pending lifecycle, then receives creation expiry on materialization. AsyncTest.asyncExpiry_pending
pins a pending update discarding a real prior duration; asyncExpiry_completion pins the
subsequent creation callback. A future that was already complete when inserted was weighed and
dated by that write, so finalizing it again charged the creation as an update: the user'sexpireAfterCreate ran at the install and their expireAfterUpdate a moment later, and
the update's duration is the one the entry kept. Measured with an Expiry of 1h on create
and 1m on update, put(k, completedFuture(v)) and a loader returning a completed future
both produced a 1m entry where the synchronous cache and an in-flight future produced 1h.
So the callers pass whether the insertion deferred its accounting and the completion skips
the replace when it did not. A ready future replacing a live entry likewise takes one
update rather than two. Reachability is not exotic: any loader that can answer without I/O
returns a completed future.
Three things about the shape are load-bearing. The readiness is read before the store,
because a future observed as ready before the install was ready when the install evaluated
it, whereas one observed afterwards may have completed in between, and skipping there
leaves the entry holding weight 0 and the sentinel permanently (measured: moving the read
after the put stranded 2,950 of 20,000 entries against a completion race, versus 0 of
500,000 with the read where it is). The decision cannot live in AsyncExpiry: itsexpireAfterUpdate receives the key, the new value and the current duration, so a
completion's replace(k, f, f) and a genuine put of an equal ready future are identical
from inside it; the sentinel test only works for the deferred case because the entry still
carries the mark. And it must not be AsyncExpiry.expireAfterCreate deferring every
creation — the synchronous view's asMap() compute family installs completed futures
with no completion handler at all, so deferring strands them at the sentinel and the user'sexpireAfterCreate is never called (9,013 failures across the narrowed async matrix when
tried).
The flag is conservative rather than exact, so the write re-tests the mark. Reading
before the store rules out one direction only. A future observed as ready was ready at the
install, but one observed as unready may still complete before the install evaluates it, and
the stale flag then charges as an update a creation the install already accounted for.
Measured on the shipped defaults with an ordinary future and an Integer key, four to nine
puts in every 200,000 take that path, and a key whose hashCode completes the future makes
it deterministic, yielding a 59s entry for the 1h/1m Expiry above. No read point fixes it,
since readiness is monotonic and neither side of the store is the observation the install
used, and narrowing the window is not a repair. So replace asks the entry instead. A quiet
write, which only a completion performs, preserves the variable time when the entry no
longer carries the sentinel, and still finalizes the weight and the write time. Nothing else
can have cleared the mark, because the two read-extension paths above return early on it.
MAXIMUM_EXPIRY = ~150 years (Long.MAX_VALUE >> 1). User-provided variable expiration
durations are clamped to prevent nanoTime arithmetic overflow; fixed expireAfterWrite andexpireAfterAccess durations are stored as given (see Timing and arithmetic in
ruled-out for a near-MAX one under a scheduler). now + ASYNC_EXPIRY
overflows to negative after ~73 years of JVM uptime, but this is within the
documented assumption that JVM instances don't run for 73+ years continuously.
accessTime uses opaque write, not CAS. CAS on every read would cause contention
storms on hot entries. Backward movement only causes benign early expiration, which
is acceptable. Variable time CAS (casVariableTime) is justified becauseexpireAfterRead can change duration arbitrarily.
Read-path expiry extension can briefly resurrect a just-expired entry — accepted.
A reader that observed an entry live and then extends it (tryExpireAfterRead'scasVariableTime for variable expiry, or setAccessTime for expireAfterAccess) can land
the extension just after the entry crossed its boundary, leaving it visible slightly later than
expiry. This is inherent to lock-free read-extension over lazy expiration: the expired entry
keeps its timestamp until maintenance removes it, so the CAS — which only checks the field is
unchanged — cannot reject an expired entry, and any fresh-clock guard before the write still
races a context switch between the read and the write; only a read-path lock (rejected) would
close it. The window is a few instructions for a normal Expiry callback; a wide one requires a
slow expireAfterRead (callback misuse, like a slow Weigher). So "never visible later than
expiry" is best-effort for read-extension — the over-stay is bounded by one duration and
self-heals on the next maintenance. Don't add a fresh-clock re-check guard for this
over-stay — it still races a context switch and can't reject an already-expired entry.
(Distinct from the node.getValue() == value value-identity check that casVariableTime
does carry: it stops a read duration rebinding onto a replaced value and is load-bearing;
keep it. It covers a replacement during the expireAfterRead callback because a loadLoadFence
orders the deadline the CAS expects before the value it checks, pairing with setValue'sstoreStoreFence as hasExpired's fence does. Without it, aarch64 satisfied the value load first,
so a reader saw the replacement's deadline with the old value, passed the check, and gave the
replacement the old value's read duration, later than its own when that duration is longer
(ExpiredReadTear.ReadExtension). It does not cover a replacement in the few instructions between
the check and the CAS: an update that keeps the exact deadline, as an expireAfterUpdate
returning currentDuration does, leaves the timestamp the CAS compares unchanged, so that
replacement takes the read's duration, a same-deadline rebind bounded by one duration like the
over-stay above. The adapter accepts the same race for its native timer.)
Bulk reads evaluate expiry at a single scan-wide now, by design. getAllPresent
(and containsValue) read expirationTicker() once and reuse that now for every
element's hasExpired/setAccessTime/tryExpireAfterRead. This gives the batch
internal consistency — every key judged at one instant, a point-in-time snapshot. A key
late in a long scan can therefore be returned present just after a concurrent single-keyget (fresh now) reported it expired (a LATE-direction over-stay). That's accepted
best-effort: a concurrent single-key read can always disagree with a bulk read on a
boundary entry under lock-free expiration, the over-stay self-heals on the next
maintenance/access, and it lasts no longer than the scan (measured at up to about 2 ms for
10,000 keys and 140 ms for 1,000,000), inside EXPIRE_TOLERANCE unless a userExpiry.expireAfterRead callback is slow (callback misuse). Don't "fix" this by re-reading
the ticker per element — it judges keys of the same call at different instants (a downgrade
of the snapshot) and adds a nanoTime read per key on the hot path.
A removal's cause is attributed at the instant that removal is performed, and clear() is not
an exception to that. clear() reads the ticker once and passes that now to removeNode,
which amortizes the read across the entries it removes under the eviction lock and calls a
user-supplied Ticker once rather than once per entry while holding it. It is a cost decision,
not a point-in-time guarantee, and it cannot become one: the sweep breaks out as soon aswriteBuffer.size() reaches WRITE_BUFFER_MAX / 2, and every straggler goes through the public
per-key remove(key), which reads its own clock. Measured on 200,000 entries with the deadline
crossed after the sweep began, one writer running: the prefix covered by the captured clock was 5
and 18 entries in two of three trials, with the remaining ~199,990 attributed EXPIRED by their
own reads. Uncontended, the same call reported all 200,000 EXPLICIT. Across methods the gap is
the same thing from outside: with a ticker stepping 1 ms per read and the deadline set midway
through, invalidateAll() reported 1,000 EXPLICIT where invalidateAll(keys), a loop of
independent remove(key) calls, reported 250 EXPLICIT and 750 EXPIRED. So cause counts are
not comparable between the two bulk calls and are not stable within one clear(). The consequence
is confined to the removal cause and evictionCount, both best-effort. Don't read the capturednow as a snapshot: making it one needs an internal removal variant carrying it through the
straggler path, and dropping it puts a per-entry ticker call back under the eviction lock.
A hit probes the value future's readiness only where the answer is consumed. hasExpired
is timestamp-only, so a reader probes isComputingAsync solely on an expired verdict, and the
successful-read blocks in getIfPresent and computeIfAbsent's optimistic hit testexpiresAfterRead() (access or variable) before their own probe, since setAccessTime andtryExpireAfterRead early-return otherwise. Async.getIfReady calls isDone,isCompletedExceptionally and join, each an acquire load of CompletableFuture.result that
the JIT cannot elide or coalesce, so a maximumSize-only async hit was paying six of them for
a result no branch could read. Measured on AsyncGetPutBenchmark.read_only (M3 Max, JDK 26, 8
threads, 3 forks, ABA): 238–246M ops/s before, 370–384M after, +53%; an acquire load isldar on arm64, so x86 should gain less. The probe count per healthy hit is 0 with no
expiration or with expire-after-write, 1 for expire-after-access and 2 for variable expiry (the
extra one is AsyncExpiry.expireAfterRead's own getIfReady), pinned byBoundedLocalCacheTest.getIfPresent_readinessProbes. Don't cache the first probe's answer to
save the second: a future can complete, or be obtruded, between the two observations.
The expiry read protocol pairs timestamp-before-value reads with value-before-timestamp
writes. A lock-free read must never return a value whose EXPIRED notification a concurrent
rewrite already fired. hasExpired is therefore timestamp-only and every lock-free reader
consults it before loading the value; a loadLoadFence at the end of hasExpired (repeated
before tryExpireAfterRead's value-identity check) and a storeStoreFence in the generatedsetValue hold both orders on weak memory, and put
stores the value before setWriteTime (the other rewrite sites already did). A reader that
observes a fresh timestamp therefore observes the rewritten value, closing the LATE direction.
The EARLY direction (stale timestamp with the fresh value) is one spurious miss that
linearizes between the old value's expiry and the rewrite, a 64-bit timestamp read being
atomic, unless a reader already returned the fresh value: a writer preempted between its value
and timestamp stores across the old deadline shows the value, a miss, then the value again. The
other non-linearizable windows are the read-extension resurrection and same-deadline rebind
above, the bulk single-now scans, a read preempted after its clock sample (it judges a value
published later against that clock, so it can return a replacement past its deadline, even one
created expired), a compute whose function outlasts its input's remaining lifetime (a
concurrent read reports the input expired before the result is stamped with a fresh clock),
and a put or replace over a live entry, which judges liveness under the node monitor before
calling expireAfterUpdate (a read crossing the old deadline in between reports the key absent
while the write returns or replaces that value with REPLACED; a few instructions for fixed
expiry, the callback's run time for variable expiry).
Each is bounded; closing them needs a read-path lock or a second ticker read per read, both
rejected above, or applying the function twice, which the compute contract forbids. A caller
acting on an expired verdict must exempt an in-flight async load by
probing isComputingAsync against a value loaded after hasExpired returns. writeTime's
setter is opaque (it was plain, a formal tearing gap on 32-bit VMs), and the swap in put
shifts a nanosecond refreshIfNeeded window from suppressing a refresh to launching one
wasted best-effort reload, absorbed by the reservation re-check and the ABA commit guards.
Pinned by ExpirationFrayTest.getIfPresent_expiringRewrite_neverReturnsExpiredValue (failed
on the first iteration before the reorder) and the ExpiredReadTear jcstress test, whose
old-modes model reproduced the tear at 0.21% of samples on aarch64 while the fixed pairing
produced zero across a tough-mode soak. The measured price on the M3 Max is nil: the reader
fence sits within run-to-run drift on HotEntryBenchmark's expiring configs, and the writer
fence is below GetPutBenchmark.write_only's noise floor (interleaved baseline/fixed forks;
that cell drifts more between forks than any fence effect, so judge it with paired runs, not a
plain before/after). Don't move a reader's getValue() above its hasExpired call, and don't
reorder a writer's setValue below its timestamp stores.
Expiration eviction is capped at EXPIRATION_THRESHOLD (1000) entries per maintenance
cycle. expireAfterAccessEntries (shared across its window/probation/protected deques),expireAfterWriteEntries, and the variable TimerWheel.advance each evict at most this
many entries, then set PROCESSING_TO_REQUIRED so rescheduleCleanUpIfIncomplete re-arms
and the backlog drains across subsequent cycles — mirroring drainWriteBuffer's cap and the
climber's QUEUE_TRANSFER_THRESHOLD. The cap is high enough that normal traffic never
reaches it; it only bounds the abnormal spike where a cache with no Scheduler goes idle,
lets a large population expire logically, then returns to traffic — one maintenance cycle
would otherwise evict the whole backlog under evictionLock, stalling any writer that
overflows the write buffer and assists (post "Assist maintenance directly when the write buffer is full"). The work isn't reduced, only
sliced, and since eviction runs async by default the slicing keeps a single cycle from
blocking a thread too long. The timer wheel rewinds nanos to previousTimeNanos when
its budget is exhausted and restores the unprocessed bucket remainder, so the next advance
reprocesses the backlog. Empty buckets rescan cheaply, but a large advance can cause
rescheduled survivors to be revisited after a rewind. The eviction check keeps non-expired
nodes from being evicted early. A capped cycle can briefly leave expired entries counting
toward weightedSize, so a same-cycleevictEntries could pick a live victim over an expired one; negligible — frequency-based
selection favors the cold expired entries and it self-corrects next cycle. Don't flag the
cap as under-expiring, and don't remove the PROCESSING_TO_REQUIRED re-arm.
The expiration scans hold no entry across a callback except their captured tail. evictEntry
delivers the removal notification; under executor(Runnable::run) or the rejection fallback the
listener runs inline, and a RemovalListener is permitted to modify the cache, so a nestedmaintenance() can unlink, transfer, or reorder any entry before the scan continues. Reentrancy
is not a supported style and cannot be detected or refused, so the requirement is only that it
not corrupt or wedge the scan. After evictEntry a scan therefore resumes from its deque's head
rather than the successor it read before the callback, and it repositions with a baremoveToBack, since the entry it holds is always its deque's head. Without a nested cycle the
head is that successor, because each step removes the head or moves it behind last; an entry
that evictEntry resurrects is examined again. Resuming on the held successor fails three ways:
- An unlinked entry has both links null, so
unlinkrunsfirst = next; last = prevand
discards the whole deque, leaving every entry in it live indataand in no eviction queue. - A transferred entry carries the links of the deque that now holds it, so
unlinksplices
that deque and assigns one of its nodes as this deque'sfirst/last. The window, probation,
and protected deques share one pair of link fields on the node, soAccessOrderDeque.containsanswers "linked somewhere", not "linked here". - A successor moved behind
last(a listener that renews entries and runscleanUp()) leaves
every entry the walk meets fresher thanlast, so the walk reorders them forever.
Guarding the reposition withcontainsand the queue type covers only the first two. Counting
nested cycles would need a field and would see only relinks made bymaintenance(). Pinned byBoundedLocalCacheTest.maintenance_recursive_accessOrder/_writeOrder,expireAfterAccess_promotedDuringScan, andExpirationTest.expire_removalListener_renewsScanSuccessors, and searched byReentrancyFuzzer, which found the relinked-successor loop within a minute of fuzzing from an
empty corpus. Don't resume a scan on an entry read beforeevictEntry.
Expiration scans re-check their captured tail after callbacks. A scan ends once it has
examined last, and reorders do not consume the eviction budget, so a nested cycle that removes
or transfers last leaves entries cycling indefinitely under evictionLock. While last stays
linked the walk reaches it, because between evictions every step moves one entry from in front oflast to behind it. After evictEntry, a scan with work remaining checks tail membership, and
for access order the tail's queue type as well, since membership answers only "linked somewhere";
the write-order links are exclusive to the one write-order deque, so expireAfterWriteEntries
needs only membership. If absent, it exhausts the budget so PROCESSING_TO_REQUIRED starts a
fresh scan. A scan that reached its own tail needs no re-arm. The timer wheel instead uses its
field-backed pending sentinel and rejects nested advances with advancing. Pins:maintenance_recursive_accessOrder_removedTail and maintenance_recursive_writeOrder_removedTail.
The wheel budget counts only evictions, never the cascade (rescheduling a non-expired
node to a finer level) — mirroring the deque caps, which count evictEntry but not themoveToBack reorder. Cascading a densely-populated coarse bucket is O(n) and not sliced,
but it's accepted: an O(1) pointer splice with no CHM write or listener (~1–2% of an
eviction), done at most once per node per advance, and a given coarse bucket cascades only
~once per its multi-day span. The only trigger is a whole cache landing in one coarse bucket
— entries scheduled past the ~6.5-day overflow span (a JVM won't outlive it) or bulk-loaded
at startup with periodic reload (a cache anti-pattern). Debated and declined (2026-07-03):
capping it bounds a lock-hold no worse than one already-accepted post-cap eviction cycle
(the equivalent threshold is ~50–100K), for the cost of new concurrently-mutated state in
the wheel. Critically, a cascade cap must never reuse the eviction rewind: the rewound
re-advance re-scans the finer levels the cascaded nodes moved to and re-cascades them,
starving the eviction drain (or livelocking). Evictions can rewind only because an evicted
node is gone, so the re-traversal skips it. Don't cap cascades via the rewind.
If ever revisited (needs a repro — a coarse bucket with ~10^6 live entries pinningevictionLock past the eviction cap while a writer-assist blocks): a safe cascade cap needs
a forward-carried backlog, not a rewind — the wheel's analog of the climber's adjustment
carry-over. On hitting the budget, stitch the unprocessed remainder (the current bucket's tail
plus the un-visited buckets/levels, reusing the nodes' existing variable-order links) into a
backlog list held on the wheel, and let nanos advance normally. The next advance flushes
that backlog first (evict the due, reschedule the rest), then resumes the level walk — forward
progress, nothing re-scanned. The hard part is lifecycle reconciliation between advances: a
backlogged node that gets descheduled unlinks transparently (same links), but reschedule
must move it out of the backlog and back into a wheel bucket, and the flush must tolerate the
list shrinking under it. That concurrent-mutation surface in the codebase's most intricate
structure is why it's deferred, not the mechanism itself.
Collected references are drained at REFERENCE_THRESHOLD (1000) per queue per maintenance
cycle, then PROCESSING_TO_REQUIRED re-arms the backlog like every other budget here. The
budget counts polls rather than evictions, so a run of stale references cannot extend the
hold, and each queue gets its own so both make progress. This is the expiration cap's shape
with a different trigger: a garbage collection clears an arbitrary number of keys or values at
once, with no user action to blame it on, and the drain then ran to exhaustion underevictionLock. Measured (M3 Max, JDK 26, 1M weak keys cleared and enqueued at once): onecleanUp() held the lock 130–190 ms; capped it is 0.12–0.5 ms per cycle over 1000 cycles,
and a concurrent writer completed 2.8k–81k operations during the backlog where it had completed
~1,100. The cost is ~45% more total drain time from the extra cycles. What the cap does not
do is bound a concurrent operation's wait: rescheduleCleanUpIfIncomplete re-submits
immediately on the common pool and evictionLock is not fair, so the drainer barges back in
and the backlog as a whole still holds the lock (a probe acquiring and releasing every 100 µs
got in 2–92 times over the backlog, capped or not). That residual is shared with the expiration
budget and is a lock-fairness question, not a reason to drop the cap or to widen it.
The maintenance consumer never waits for a producer's publication. MpscGrowableArrayQueue
publishes in two steps, a CAS of the producer index and then a release store of the element, and
the strong poll() spins on that element when the index says the queue is non-empty. Both
consumers (drainWriteBuffer and clear) hold evictionLock, so a producer descheduled
between those two instructions — a container throttling a thread, say — stalled every other
policy operation for its whole pause and burned a core doing it. Both use relaxedPoll(), which
returns null instead, and JCTools offers exactly that pairing for a consumer that must not
block. Nothing is stranded: every production offer is afterWrite's, and scheduleAfterWrite
runs after offer returns, so the passed-over task is re-armed by its own producer (IDLE →
REQUIRED, or PROCESSING_TO_IDLE → PROCESSING_TO_REQUIRED, which makes maintenance's final CAS
fail). clear is safe for a second reason: it already abandons the buffer once a concurrent
writer refills it past WRITE_BUFFER_MAX / 2, AddTask links only if (isAlive), deque
removal is contains-guarded, and makeDead takes the weight from the node rather than from the
policy precisely because an update may still be buffered, so a task that outlives the entry it
describes costs a weight swing that telescopes back to zero. It ends withrescheduleCleanUpIfIncomplete, which is what carries the remainder forward.
Exception Handling
Checked-exception conversion and the listener guards restore interruption. Kotlin, Scala,
and Groovy callbacks can throw InterruptedException through Function, BiFunction,CacheLoader, or RemovalListener; checked exceptions are a javac rule, not a JVM constraint.Caffeine.toUnchecked rethrows Error, returns RuntimeException by identity, restores the
current thread's interrupt status for InterruptedException (JDK interruptible waits clear it
when throwing), and otherwise wraps in CompletionException. The defaultCacheLoader.asyncLoad converts inside its supplier, on whichever thread the executor uses.
Conversion is used by loader chains, catch-commit-rethrow for COLLECTED/EXPIRED recomputation,
and AsyncBulkCompleter. Absent-key paths and UnboundedLocalCache propagate unchanged and need
no conversion-side repair. The eviction and removal listener guards log the failure instead of
converting it, so they restore the status before logging: the eviction listener runs on the
thread doing the eviction, often the caller's own put or compute, and a removal listener
runs on the caller under a caller-runs executor or a rejected submission. Later interruptible
calls on that thread then fail at once, including the listener's for the next entry evicted in
the same pass; that is the interrupt taking effect, not a cascade to contain. The statistics,
scheduler, and future-completion guards do not restore, since the callbacks they wrap are not
expected to wait.
Catch-commit-rethrow pattern in doComputeIfAbsent and remap. Both catchThrowable, not just RuntimeException. When user code
(mapping function, weigher, expiry) throws after notifyEviction was called, the
phantom eviction is made real: the node is retired, null is returned to CHM, and the
exception is deferred past cleanup.
notifyEviction is called BEFORE user code, not after. This can't be reordered —
it preserves linearizability for resource-based listeners (e.g., file delete before
recreate). The catch-commit-rethrow pattern handles the case where user code then
throws.
wasEvicted flag in remap: boolean wasEvicted = (ctx.cause != null) is captured
BEFORE the try block because ctx.cause can change from null to REPLACED
inside the try. The catch block uses !wasEvicted to distinguish eviction-path
exceptions (commit+defer) from non-eviction exceptions (immediate rethrow).
remap's no-op exit records itself in ComputeContext.unmodified; the post-write dispatch
must not re-derive it. The in-lambda short-circuit takes the exit on four conditions
(preserveTimestamps, a same-instance return, and no removal cause); the dispatch used to test
only the hint, so a hinted call that did mutate committed the mutation and then skipped itsAddTask/UpdateTask. A hinted update left weightedSize short by the delta forever, and a
hinted create on an absent key (the absent branch has no short-circuit at all) installed a node
linked into no deque and counted in no weight, which makeDead later subtracted fromweightedSize anyway, relaxing the bound by that much per cycle. Not reachable from the public
API: every caller that sets preserveTimestamps returns the instance it was handed, and each
insert branch returns before the hint is set. It is latent because RemapHints is
package-private, so nothing warns a future caller. Pinned byBoundedLocalCacheTest.remap_preserveTimestamps_newValueDiffers_publishesTheUpdate andremap_preserveTimestamps_absentCreate_publishesTheAddition. Don't restore a second copy of the
predicate; the exit that skips the work is the one that says so.
remap same-instance return is a setter no-op, NOT a metadata no-op. When a
user compute/merge remapping function returns the same value instance as the
current value, setValue is skipped, but weight, accessTime, variableTime,
and writeTime still update. This is intentional: compute is a mutation API,
so a same-value return is still treated as a write for expiration, refresh eligibility,
and eviction policy, subject to timestamp tolerance. The only documented full no-op is the
explicit preserveTimestamps path. A reader expecting compute(k, (k, v) -> v) to leave eviction
ordering and expiration deadlines unchanged would be surprised; the source does not call this
out, so this entry is the canonical place the behavior is documented (preferred over a source
comment).
Value-bearing user callbacks propagate; fire-and-forget callbacks are guarded.Ticker, Weigher, Expiry, and loaders return information the cache needs, with no safe
default. Stats, scheduling, and listener wrappers can instead return an empty/default result.
Do not add containment for broken equality, hostile futures, or JVM errors. The precedent is
to fix the user component (as Quarkus did for its broken future), not add defensive code or a
must-not-throw note to Ticker.
A value-bearing throw can land after a commit. Examples include AddTask / UpdateTask
updating policy totals before a ticker read, and refresh/async prologues reading statsTicker
before cleanup. Such failures can skew accounting or strand tokens/proxies; those mechanisms
are accepted under this boundary, not denied. Broken key equality can cost the size bound. AhashCode that throws in evictEntry during an UpdateTask skips that update's weightedSize
delta, relaxing the bound by the delta for the cache's lifetime; one that throws when admission
compares frequencies stops size eviction, logging each failed cycle, until it stops throwing,
because every removal hashes the key too. Containment often invokes the same broken component.
Preserve existing targeted cleanup, including refresh completion's own-token catch; the boundary
is not a reason to remove it.
Concurrent standard-future obtrusion is the supported exception. Between readiness checks
and join, an obtrudeException can make Async.getIfReady throw. A plain-future stress run
produced 3.5M throws in 1.28B query rounds. Its narrow CancellationException /CompletionException catch now returns null as the method promises, including to maintenance
under evictionLock; other hostile-subclass exceptions propagate. Completion handlers are
one-shot, so obtruding after success can leave a physical entry that queries filter. That is
accepted, although the entry never expires: evictEntry reads the failed future as in flight and
re-dates it with the async sentinel, so only size eviction or an explicit removal reclaims it.AsyncBulkCompleter.failProxies removes before obtruding.
A maintenance throw defers buffered work; it does not drop it. The final CAS can settlePROCESSING_TO_IDLE → IDLE on a throw, leaving unprocessed tasks in the buffer. Later writes
re-arm, and a full buffer forces inline assist. Do not force REQUIRED or add a re-arm toperformCleanUp's throw path. The inline fallback's own write task is different: it must run
in maintenance's finally, preserving the repair for that task's former loss.
Executor rejection and lock contention need different epilogues.scheduleDrainBuffers deliberately does not follow rejection-fallback maintenance withrescheduleCleanUpIfIncomplete. The rejecting executor cannot use the common-pool arm; a
pacer would submit to the same broken executor, where its fire-time rejection is swallowed.
Recovery relies on later activity, as documented for discarded/never-run
tasks. A drain that merely bounces off clear's held lock still has a healthy executor and
must reschedule. Do not generalize the rejection exception to that case.
Buffers
MpscGrowableArrayQueue is a shaded JCTools port and is not wrap-safe; that is accepted. AtpIndex == cIndex == 2^63 - 256 the "is there room" sum overflows and a resize is selected on an
empty maximum chunk, stranding the odd producer-index marker so every producer spins at 100% CPU. It
needs 2^62 offers on one never-reset queue (millennia at a measured 10-20M put/sec), and upstream
master is identical. Don't harden it: (pIndex - cIndex) < bufferCapacity does not suffice
(producerLimit stores the same wrapping sum), and rolling the index back after producerBuffer = newBuffer strands the consumer, which is worse than the spin.
StripedBuffer.offer treats FULL as success and only expands on FAILED. A failed CAS is
a contention hint; weak CAS can also fail spuriously. A full buffer only means the drain is behind,
which striping does not fix. Routing FULL into expandOrRetry would let one thread's
routinely-full buffer grow the table and allocate stripes nobody contends for. So a FULL home
stripe returns FULL without probing a sibling, which is what makes a stalled stripe skip that
thread's reads until it drains. Both are intended: the FULL return is the signal that tellsafterRead to drain. Don't make FULL probe for another slot.
A thread's starting stripe never moves. The probe is re-derived from the thread id on everyoffer, so the incremental hashing in expandOrRetry varies the slot only within one call. This is
forced, not chosen: ThreadLocalRandom.getProbe/advanceProbe, which Striped64 uses to
permanently move a colliding thread, are package-private to java.util.concurrent. Don't "restore"
the convergence search; the only alternative is a ThreadLocal on the read hot path.
References
The keyReference in a weak/soft value reference is read and written opaquely. The
field is set during construction and, under synchronized(node), mutated to a sentinel
value (RETIRED_*_KEY / DEAD_*_KEY) when the node is retired or dies. Lock-free
readers tolerate the resulting staleness window as weakly-consistent observation, but a
strong-key weak/soft-value node keeps its key inside that reference, so getKey() andisAlive() read the field independently: with plain reads, a reader that observed the
sentinel and then observed the older key would judge a retired node alive while handing
out an internal sentinel as its key. Opaque access forbids exactly that, since opaque
operations on one variable are coherent, and it costs nothing at runtime because the
constraint binds the compiler rather than the hardware (opaque loads compile to ordinary
loads on x86 and aarch64 alike). The field stays non-volatile and the constructor's store
stays plain, the object not yet being published; only the accessors are opaque. A jcstress
probe was written to arbitrate first and found no violation over both weak and soft values
while exercising the window (it observed the monotonic key-then-retired interleaving),
so it was discarded rather than kept as a pin: the hazard is a legal compiler
transformation that a green run cannot refute.
In setValue, a new WeakValueReference is installed viasetRelease followed by VarHandle.storeStoreFence(), then the old reference is
cleared via ref.clear(). The fence prevents the old reference's clear() from
being reordered before the publication of the new reference. Without the fence, a
reader that re-reads the same reference and observes a cleared referent cannot
distinguish "the clear was already committed" from "the clear's store buffer is
ahead of the new reference's publication" — breaking the getValue re-check loop
invariant. The reader's loadLoadFence pairs with that ordering through the observed clear;
this does not require first acquiring the replacement reference. See
RA fences and their specializations.setRelease alone orders the new reference's constructor writes before
the publication, but does not constrain the subsequent ref.clear() against any
racing reader (#1820, confirmed on aarch64 M3 Max via JCStress IntermittentNull test).
In the constructor, a plain VALUE.set is used since the object itself is not yet
published. Strong-value setters also use a store-store fence, to order the value before
subsequent timestamp stores as required by the expiry read protocol;
they have no reference-clearing operation to order.
Weigher.boundedWeigher wraps all user weighers and enforces weight >= 0 at
runtime via requireArgument.
Concurrency
maximum and weightedSize have a plain reader and an acquire reader, and no public-facing path
may use the plain one. AddMaximum.addPlainAndAcquireField emits maximum as a VarHandle.get,
which guarantees bitwise atomicity only up to 32 bits, so a plain read of the 64-bit field can tear
against setMaximum's release write on a 32-bit JVM. The plain reader is for callers already
holding evictionLock; everything reachable from the public API takesmaximumAcquire/weightedSizeAcquire. There is no way to assert an access mode in a test, so this
is a review-time invariant: check the call site's lock state, not the accessor's name. The three
duration fields need no such care, since addAcquireReleaseField emits only an acquire reader.
No debug-mode assertions. Runtime invariant assertions are impractical for
concurrent code — too hard to assert on a running system. Correctness relies on
testing (Fray, LinCheck, JCStress) and static analysis (ErrorProne @GuardedBy).
nanoTime is monotonic. Per JVM spec, System.nanoTime() is monotonic. Backward
movement would be a JVM bug, not a cache issue.
scheduleAfterWrite's IDLE arm retries its failed swap rather than acting on what it read.
The status can advance between the opaque read and the compare-and-swap, so a writer that readIDLE may find PROCESSING_TO_IDLE. Dropping the failed swap and calling scheduleDrainBuffers
anyway leaves that write with no driver: the guard sees a drain in flight and returns, while the
drain has already passed the task's slot, and its exit swap then settles the machine at IDLE
with the task still buffered. Retrying against the observed status routes to the processing arm,
which converts the exit to PROCESSING_TO_REQUIRED — the same shape the processing arm has always
used, and why the two arms now look alike. The end state it prevents was benign (the task is not
lost; the next write, a cleanUp, or a read that fills a stripe drains it, which is the deferral
already priced for the maintenance-throw path and the WRITE_BUFFER_MAX cap) and reaching it
needs the entry store's visibility to lag the drain's loads, so this is a tidiness fix, not a
correctness one — recorded because three audit runs have now reached for this machine. IDLE is
written in exactly one place, the value-checked casDrainStatus(PROCESSING_TO_IDLE, IDLE), so noREQUIRED is ever swallowed. Don't collapse the arm back to an unconditional swap. Pinned byBoundedLocalCacheTest.scheduleAfterWrite_staleIdle_retriesAgainstTheObservedStatus.
Read recording starts with the frequency sketch. A fastpath cache (bounded, strong keys and
values, no access expiration) starts with the disabled read buffer, so its reads skip the eviction
policy until the sketch is initialized: at half occupancy, when presized by initialCapacity, or
when setMaximum makes the cache half full. Each of those sites calls recordReads(), which swaps
in a BoundedBuffer under the eviction lock. A test that initializes the sketch directly must callrecordReads() as well; BoundedLocalCacheTest.fastpath, fastpath_presized andfastpath_setMaximum pin the three sites.
readBuffer is volatile, so the swap is a safe publication. A plain field is also safe: a new
buffer's constructor writes nothing, and JLS §17.4.4 makes default values visible to every thread.
But that holds only while StripedBuffer stays stateless at construction, plain reads have no
guarantee of ever observing the swap, and race detectors flag it (a TSAN report, #2020). An
acquire read, as drainStatus uses, would also suffice, but C2 compiles the two identically on
every port except PPC64, the only one where a volatile load gets a leading sync. The volatile
read was kept because the JLS memory model formally defines it, while the other modes lack a full
formal specification
(Lea, JDK 9 memory order modes). On an Apple M3
Max, plain and volatile measured within 3% of each other on JDK 11 and 27 inGetPutBenchmark.read_only, and on 11, 26 and 27 in a harness matching it. A JDK 26 JMH run with
release-26 bytecode showed volatile 24% faster under identical inlining; it did not reproduce on
11 or 27, so it is a C2 layout artifact rather than a cheaper read.
The hit path offers to the buffer without testing the sketch. The skipReadBuffer() check it
replaced loaded sketch.table, which shares a cache line with size, and the maintenance thread
writes size for nearly every drained read. On a read-only JMH sweep of a presized 65,536-entry
cache (Apple M3 Max), removing the check took a hit from 11.65 to 9.99 ns at one thread and from
11.13 to 7.00 ns per thread at eight, and a cache under half full was no slower. Two alternatives
were measured and rejected: padding size 128 bytes away from table recovered less (12.21 to
10.35 and 10.28 to 9.02 ns/op) at 128 bytes per cache, and keeping the count in the table's
trailing slot recovered a third of the gain at one thread and none at eight, for a reason not
established.
The maximums and weighted sizes (maximum, weightedSize, and the window and protected pairs) are
padded from the fields that a hit loads, because the maintenance thread writes them for drained
writes, promotions and climber steps. Unpadded, they shared a 128-byte line with an expiring or
refreshing cache's ticker and duration (expiresAfter*Nanos, refreshAfterWriteNanos) at every
alignment, and with the stats counter and the base class's data, nodeFactory and readBuffer
at others; a line that also held drainStatus, which every drain writes anyway, cost little. On an
Apple M3 Max with eight readers and a writer doing 500k put+invalidate/s, averaged over the cache's
16 alignments in a line on JDK 27, padding cut the mean time per hit (eight readers over their
throughput) from 28.1 to 24.6 ns for a size and expire-after-access cache, from 23.1 to 21.8 ns
with stats, and from 17.1 to 16.0 ns for a size-only cache, whose put+invalidate fell from 399 to
329 ns. JDK 25 agreed, except that the size-only hit was within noise there.
So AddMaximum.supertypes puts a class of 15 longs (120 bytes) before the class that adds the six
fields, and another between that class and its expiring and refreshing subclasses: 120 bytes per
cache bounded by size or weight, 240 with expiration or refresh. The class that adds them holds only
those longs, and its climber, sketch and access-order deques go in a class before the first pad,
because HotSpot (JDK 15 and later) places a subclass's fields in its superclass's alignment gaps:
those references leave a 4-byte gap, and a subclass's ticker filled it beside the counters. Each
pad is a class of its own because a class's own fields of one size may be laid out in any order (JDK
17 and 21 sort them with the platform's qsort, which on macOS swaps the first and last of the run;
JDK 24 made the sort stable, JDK-8341471). A size-only cache's end is left unpadded: the object
after it is the collector's choice (the eviction lock at allocation, the write buffer's leading pad
after a young collection on G1 or Parallel), and the gains were the same with either beside it.
Moving the six fields into a separate object would cost about 20 bytes, but that object shares its
line with whatever the collector places beside it, which JOL cannot check. The JDK pads its own
separate counter objects (ConcurrentHashMap.CounterCell, Striped64.Cell) for that reason.BoundedLocalCacheTest.evictionFields_ownCacheLines checks the layout with JOL; only JDK 15 and
later fill gaps, so only they can fail it.
Node State
Two weight fields: weight (entry's perspective, guarded by synchronized(node))
and policyWeight (policy's perspective, guarded by evictionLock). They're correlated
but updated at different times — this is intentional for the telescoping sum to work.
makeDead subtracting the finalized getWeight() (not policyWeight) and UpdateTask.run
being deliberately dead-guard-free are a matched pair: a late-applied UpdateTask adds back
exactly the δ that makeDead over-subtracted. Don't add an isDead guard to UpdateTask and
don't switch makeDead to policyWeight — either one alone breaks the cancellation.
Both retire() and die() preserve the queue-type bits, so late updates use the same region
classification as makeDead. The same reconciliation applies to global and regional weighted
sizes; until late updates arrive, a region counter need not equal its deque's policy-weight sum.
Because racing updates offer their UpdateTasks outside the node lock, out-of-order
drains can leave a live node's policyWeight transiently negative; the climb transfer
loops then charge that weight to their quota and over-shift the region caps beyond the
commanded adjustment (the net can even invert the commanded direction). Adjudicated
tolerated, not guarded (2026-07, audit-adaptivity F1): the caps are the controller's
policy targets, not capacity enforcement (eviction and the total bound compare weightedSize
and each node's policyWeight with maximum, and eviction on the same transient before it
telescopes is the premature eviction ruled-out.md §Core accepts), and the split coerces back
on its own: the next completed sample overwrites the inflated carry-over, the below-floor lift
is not step-capped, and the excursion is bounded by one weigher swing per writer racing on the
key. Don't
clamp the transfer quota against negative weights, and don't "fix" the offer ordering.
Queue type constants are plain ints, not enums: WINDOW=0, PROBATION=1, PROTECTED=2.
The field is plain (not volatile), guarded by evictionLock.
ConcurrentHashMap Constraints
No recursive computations. Writing to the cache from inside an atomic
compute/computeIfAbsent/merge callback violates ConcurrentHashMap's contract; this is
not a Caffeine bug. Detection is best-effort: encountering an empty bin's ReservationNode
throws IllegalStateException("Recursive update") (surfaced raw, unwrapped), as does a
recursive append detected by compute or computeIfAbsent in a populated linked bin.
Other recursive updates, including tree-bin operations, can go undetected and silently
corrupt (lost inserts, double count updates, clobbered writes). A nested insert that crosses
the resize threshold runs the resize on the computing thread, which re-enters the held bin and
abandons the resize for the life of the map, leaving misplaced keys that iteration finds, get
misses, and invalidateAll() does not remove. Never rely on the ISE as a safety net. During a
refresh completion this can orphan the key's refreshes token (suppressing its auto-refresh)
only if data.compute throws before remap's lambda — a broken hashCode or a rare
cross-bin ISE (same-key recursion silently re-enters a populated bin instead); in-lambda throws
self-clean on the exits a completion can reach (the create-branch finally and the
present-entry catch). The one exit that preserves — an absent-branch user-function throw
(both siblings; ULC guards its catch with value != null) — a refresh completion never hits,
because its own lambda cannot throw before materialization. Either way the orphan self-heals on
the next write/removal. Don't add a catch-side refreshes.remove — it re-throws on the
broken-hashCode sibling.
The same restriction covers a read inside a computation or eviction listener that triggers an
inline refresh update or inline maintenance. Two remappers reading each other's stale key can
deadlock when completed reloads commit under the enclosing computations' locks, even with a pure
loader and the default executor, and a commit that removes a bin neighbour (a null reload)
leaves the computed key mapped to a dead node if the computation removes it. On an executor that
runs tasks on the caller (Runnable::run, a saturated CallerRunsPolicy pool), any read that
schedules maintenance runs the whole cycle under the locks the callback inherits. Its eviction of
the entry under computation or of a bin neighbour loses the computed value while the call returns
it, notifies the old value twice, drifts the weight, or leaves a dead node mapped; from put's
eviction listener it takes the bin under the node monitor and deadlocks against a concurrent
removal or load of that key. Both are still unsupported recursive modification under the existing
warnings that these callbacks must not modify the cache; neither calls for a dispatch change,
callback-scope tracking, or another API warning. A liveness check after the callback is too late:
the nested eviction has already unlinked the map's node, so the enclosing compute writes through
stale pointers whatever the lambda returns. The check could only choose between losing the value
silently and throwing, and it cannot detect a neighbour's eviction.
The evictionListener runs inside the CHM compute lambda — notifyEviction is called
within data.compute/computeIfPresent, holding the entry's bin lock — so it is subject to this
rule: a listener that modifies the cache (same-key or other-key) is a recursive update → an ISE
(caught + logged in notifyEviction, so the write is silently lost) or silent corruption. The
corruption can outlive the listener: a dead node left mapped makes every later put of its key
throw the broken-equality IllegalStateException, which blames the key's equals or hashCode,
and neither invalidate nor invalidateAll removes it. The Caffeine.evictionListener javadoc
says "must not modify this cache." That (and the parallelmappingFunction/remappingFunction warnings across Cache/AsyncCache/LoadingCache/Policy)
was tightened from the wording inherited from ConcurrentHashMap's compute javadoc — "must not
attempt to update any other mappings" (CHM's phrasing through JDK 13; JDK-8232652 replaced it with
"must not modify this map" in JDK 14, though merge still carries the old form) — which by negation
wrongly implied a same-key mutation was safe. The removalListener, by contrast, runs outside the
atomic operation (async/after the fact) and may modify the cache. Don't flag a same-key mutation
from the eviction listener as a corruption bug — it's documented misuse.
CHM never hands back a stored key, so the unbounded cache notifies with the caller's
instance. compute and computeIfPresent pass the remapping function the key the caller
supplied, and the table keeps the instance it already held; there is no API that returns the
stored one. UnboundedLocalCache is a thin wrapper over that map, so its Object-accepting
removals notify with the argument, where BoundedLocalCache reads node.getKey() and Guava'sLocalCache reads entry.getKey(). A RemovalListener therefore receives an equal key, not a
particular instance. The visible consequence is a cross-type equal key: with a stored HashSet
and an equal TreeSet passed to remove(Object), remove(Object, Object),keySet().remove(Object) or entrySet().remove(Object), all eight bounded and unbounded cells
removed the entry, but a correctly typed listener ran on every bounded one and none of the
unbounded ones, where the checkcast throws and notifyRemoval logs it as a listener exception.
Recovering the canonical key there needs an O(n) scan or a per-entry node, which is the structure
the unbounded cache deliberately does not have, so this is a boundary rather than a repair.
Identity preference in cases that lose no notification is separately unspecified.
CHM bin blocking is not a Caffeine bug. compute() locks the hash bin. If the
mapping function (cache loader) is slow, all other operations on keys in the same
bin are blocked. This is the #1 recurring user issue (~20 reports). The answer is
always: use AsyncCache for slow loaders, increase initialCapacity to reduce
collisions, or make loaders faster.
A refresh commit waits on its bin like any write. The commit is a compute run by the thread
that sees the reload complete: the refresh caller, or the read that triggered an automatic
refresh, when the reload is already complete; otherwise the thread completing it, such as an
executor worker. It waits out any computation holding the key's bin. refresh's "asynchronously"
describes the load. Dispatching the commit with whenCompleteAsync would add a task per refresh,
give up installing a completed reload before refresh returns and returning it from the
triggering read, and still run inline under a caller-runs executor. A load that holds the bin while
waiting for work on the executor completing the reload deadlocks with the commit, as when Guava'sCacheLoader.asyncReloading shares a single-thread executor with the loader's own fetches; that
wiring is the application's, under the executor rule in
standing principles.
The same doctrine covers a two-map deadlock, not just blocking, including when it wedges an
innocent thread. A loader that touches the cache can take the refreshes bin monitor and then adata bin monitor, while any write takes data and then refreshes through discardRefresh.
Opposite orders, and findDeadlockedThreads() reports it on both implementations. A read closes
the cycle as well as a write. Reading a stale key registers its refresh, which takes a secondrefreshes bin, so two reloads reading keys in each other's bins deadlock on refreshes alone, and
a nested reload that completes inline commits into a data bin. On one thread, a registration that
lands in the bin the reload holds makes CHM throw Recursive update: from the loader's read when
the bin was empty, failing the outer refresh, or from the caller's get or refresh when it was
populated. The counterparty can be maintenance evicting or expiring a key whose refresh is
registered; it waits holding evictionLock, so every later cleanUp() and every writer that falls
back to inline maintenance hangs with it. The reload body holds the monitor only on a caller-runs
executor or in an asyncReload's synchronous prefix; a CacheLoader.reload on the default
executor runs on a pool thread. It is declined on the same basis: the only path that takes another
lock while holding a refreshes bin is the user's reload/asyncReload, andCacheLoader.reload's javadoc bolds the prohibition ("loading must not attempt to update any
mappings of this cache directly or block waiting for other cache operations to complete").
Everything else inside refreshes.computeIfAbsent is either a lock-free data read
(getIfPresentQuietly) or the asyncReload call itself, and the completion handle that does thedata work is attached outside the lambda, deliberately, so the refreshes monitor is not held
while it runs. Keep it outside.
Two objections are answered rather than ignored. That the victim is an ordinary put which did
nothing wrong is true, and is a property of every lock-order inversion in a shared structure; the
counterparty of misuse is always innocent. And a silent deadlock is a worse diagnostic thanCache.get's documented IllegalStateException for recursive updates, which is a fair criticism
of the diagnosis rather than evidence the ordering is wrong. Don't reorder the internal maps to
make a forbidden loader safe.
Expiry must not call into the cache, and carries no javadoc saying so. The callback is
invoked under synchronized(prior) in put and under the bin lock on the compute paths. A
calculator that calls cleanUp() or a Policy ordering method waits on evictionLock while
holding the node monitor, deadlocking against the maintenance thread's evictEntry, which takes
those locks in the documented order. One that calls invalidate re-enters the monitor on its own
thread, retires the node, and lets the outer put commit into the dead entry. Neither is guarded.
The omitted warning is deliberate: computing a duration has no reason to re-enter the cache, so
the note that fits CacheLoader.reload and evictionListener would be noise on Expiry andWeigher. Hoisting the callback out of the monitor the way Weigher was hoisted is not available
either, since expireAfterUpdate and expireAfterRead read the node's variable time under it and
the compute-path invocations need the atomic context.
A Policy ordering snapshot or setMaximum inside an atomic scope re-enters maintenance on any
executor. Both take evictionLock and run a maintenance cycle, and Cache.policy() forbids
policy operations within another operation's atomic scope. The eviction listener runs while its
victim is still linked and counted, so a nested cycle there evicts the same victim again, through
the reentrant lock, until StackOverflowError: one due eviction became 407, and the drifted
counters let a maximumSize(2) cache hold 407 entries. From a remapping function the nested cycle
corrupts the bin like an inline eviction (see No recursive computations) or deadlocks against
maintenance. Not guarded: a held-lock check in snapshot would miss the listener's cleanUp()
and the remapping-function arms.
clear()/invalidateAll() do not wait for an in-flight computeIfAbsent insert. The insert
is invisible to clear()'s data.values() snapshot (the CHM Traverser skips the in-flightReservationNode), so it survives and serializes after the clear. CHM blocks per-bin and removes
it, but both orderings are linearizable, and the layered design can't see CHM's internal per-bin state
to block (a fix would require forking CHM). Inserts only — an in-flight update on an existing node
is waited for (clear()'s removeNode goes through computeIfPresent + synchronized(node)).
Documented user-facing on the invalidateAll() javadoc ("behavior … is undefined for an entry that is
being loaded (or reloaded) and is otherwise not present") + the wiki. Don't flag the clear-vs-in-flight-
insert divergence.
Eviction is async, not immediate. After put, the cache may temporarily exceedmaximumSize until the executor runs maintenance. Use executor(Runnable::run) for
inline eviction in tests, or call cleanUp() before assertions.
That determinism has a cost worth knowing before reaching for it under load. The expiration and
window scans bound evictions, not traversal: remaining-- runs only on an eviction or a
transfer, so a node the scan relinks and skips (an in-flight async load, a zero-weight entry) is
free and the walk continues past it. The default executor hides that, since maintenance coalesces
through the drain status and many writes share one cycle. Runnable::run removes the coalescing,
so a burst of pending async loads pays a walk over the pending set on every write in the burst,
which is quadratic across it. Fine for a test with a handful of entries; not a knob to reach for
in a benchmark or a reproduction that holds thousands of loads in flight. The expiration scans are
self-correcting once any load completes (they relink pending entries to the MRU end and stop at the
first completed, unexpired node), so their cost needs a deque with no completed entry at all. The
window scan relinks nothing, so its walk persists while pending entries sit at the window head; its
price under the default executor is in the pending-async scan entry of
ruled-out.
Expiration and cleanup are amortized, not instant. Caffeine performs maintenance
during write operations and occasionally during reads. For idle caches, useScheduler.systemScheduler() to get prompt expiration. This is best-effort with
no hard timing guarantees.
No close() by design. The Cache interface deliberately does not extendCloseable. The cache is a data structure that becomes GC-eligible when
unreferenced. PerformCleanupTask is a WeakReference to its cache, which breaks the
scheduler→cache reference chain, so scheduled maintenance becomes a no-op
when the cache is unreachable. (JCache's CacheProxy is the only component
with explicit close semantics, as required by JSR-107.)
Refresh
Refresh returns the stale value, not the fresh one. get() returns the current
value immediately and triggers an async reload. The next get() returns the
refreshed value. This is the entire point — hiding reload latency from callers.
Refresh only triggers on access. An idle cache with no reads will never refresh.
For proactive refresh, use ScheduledExecutorService with cache.refresh(key).
"Logged and swallowed" is a promise about the future's result, not about producing it.refresh documents that a failed reload is logged and swallowed, and that covers theCompletableFuture completing exceptionally: the load ran and failed, the mapping is unchanged,
and the caller who ignores the returned future sees nothing. It does not cover asyncReload
throwing synchronously, which happens before there is a future at all. Those are distinct
failures. Guava and early Caffeine did not hand the future back to the caller, which is the
wording's origin; a loader that throws while merely constructing its future has a bug in the most
basic step, and surfacing it is right. Do not "fix" refresh/refreshAll to swallow a
synchronous throw from asyncReload/asyncLoad, and do not cite the declared throws Exception
on those methods as evidence that it should be swallowed.
expireAfterAccess + expireAfterWrite together is discouraged. Inherited from
Guava for compatibility. The two timestamps are independent; whichever has the
shortest remaining duration wins. Prefer expireAfter(Expiry) for custom logic.
Iteration
EntrySet.removeIf predicates receive immutable snapshots (Map.entry(k,v)), sosetValue throws. Like CHM and Guava's JDK-8078726 repair, removal is conditional on the
captured value. This applies to bounded, unbounded, async synchronous AsMapView, and rawAsyncEntrySet; both async views delegate to the inner cache's removeIf, like their values
views. Keep write-through entries for iterator / spliterator / toArray, and do not
restore the positional iterator.remove default for predicate removal.
Map equality uses size, iteration over this map, and count == expectedSize. The
AbstractMap shape is symmetric with HashMap for equals-keyed caches and costs O(n), versus
CHM's O(n+m) two-sided scan. Under weakKeys() it is not: the cache equals a HashMap holding
an equal but distinct key, and that HashMap does not equal the cache (IdentityHashMap is
unequal both ways). Two equal-but-distinct keys also make the cache equal a map holding one of
them plus an entry the cache lacks, with unequal hash codes, as Guava's weakKeys() cache does;
only IdentityHashMap's identity pass avoids that, at the cost of making the single-key case
false both ways. The final count catches maintenance trimming dead entries after the size
prescreen; otherwise a surviving subset can incorrectly compare equal. Preserve it inBoundedLocalCache.equals and the future-typed LocalAsyncCache.AsMapView.equals.
asMap() iteration is not a cache read. Iterators do not update access times or
frequency counters. This prevents iteration from polluting the eviction policy.
Expired entries are skipped during iteration.
Under weakKeys() the key and entry spliterators still claim DISTINCT, and must. TheweakKeys() javadoc names IdentityHashMap as the model for its semantics, and the JDK's own
identity-keyed map advertises DISTINCT on exactly those two spliterators while omitting it onvalues(), which is the shape Caffeine matches. It produces the same triple audits report as an
anomaly: two keys that are equals but not == give size() == 2,keySet().stream().distinct().count() == 2, and new HashSet<>(keySet()).size() == 1 (verified
on JDK 25). Removing the flag would not be a no-op, it would be wrong: distinct() would then run
with equals, merging two genuinely distinct live entries so the stream reports fewer elements
than the cache holds. Under identity semantics the keys are distinct, so the flag is the correct
claim.
Identity comparison applies to one-sided queries, never to equals or hashCode. A
one-sided query (remove(k, v), replace(k, old, new), containsValue, values().remove,keySet().removeAll) can be identity-based unilaterally, because the worst case is declining to
act. A bilateral contract cannot: the other side is AbstractMap or AbstractSet, which compares
with equals and never reciprocates, so identity there breaks Object.equals symmetry and buys
nothing. Measured with the cache holding one value and a HashMap holding an equal-but-distinct
one: at HEAD both directions of equals are true, while an identity-comparing equals gives false
one way and true the other. This is the boundary of the _byIdentity convention, and every method
that convention covers is one-sided. No implementation anywhere compares keys by equality and
values by identity in equals: IdentityHashMap is reference-based on keys and values alike, and
Guava's LocalCache extends AbstractMap and declares no equals at all.
So value identity is coherent with no map, and that is accepted. Under weakValues() orsoftValues() the value-bearing queries compare by identity (containsValue, remove(k, v),replace(k, old, new), values().contains/remove, entrySet().contains/remove, and throughAbstractSet the entry view's equals), while Map.equals, hashCode and theWriteThroughEntry objects the views emit compare with equals. The bulk removals compare withequals as well: values().removeAll/retainAll ask the argument collection andentrySet().removeAll/retainAll compare the emitted entries, so they can remove an entrycontains reports absent or keep one, under weakKeys() too; IdentityHashMap's identity entries
do neither. Probed against
a HashMap holding an equal-but-distinct value: equals true both ways with matching hash codes,entrySet().equals false one way and true the other, contains and containsValue false, and the
emitted entry equal to the probe entry. Guava's weakValues() cache reproduces that row element for
element, though its values().remove removes by equality where Caffeine's compares identity. Both
coherent alternatives are worse. IdentityHashMap buys internal coherence and is
still asymmetric against a HashMap (equals false one way, true the other, differing hash codes),
which is the bilateral case above. Making the queries equals-based would contradict the value
semantics weakValues() and softValues() document. Reference implementations do not agree with
each other here, and the cache is biased toward size eviction rather than reference collection, so
do not raise the disagreement as a defect or propose a consistency repair.
View equality has an accepted physical-size/logical-content gap. Key and entry views
inherit AbstractSet.equals / hashCode. size() includes expired or collected entries
pending maintenance; iteration, containment, and hash code skip them. With live a and
pending-dead b, HashSet{a,b}.equals(keySet) can return true while the reverse comparison
is false and hash codes differ. The argument's HashSet.equals performs the true-returning
comparison, so overriding the view's equality cannot repair it. A logical size() was
rejected to preserve the lock-free physical estimate; a physical hash is unavailable once a
weak key is collected. WeakHashMap has the same changes-during-comparison problem.
Values views use identity equality like CHM. The same gap lets isEmpty() disagree with
iteration (an isEmpty()-guarded iterator().next() can throw NoSuchElementException),
null-pads a toArray(new T[size()]), and lets a receiver that compares entry sets (Guava'sImmutableMap) equal asMap() while holding a never-cached entry per pending one. The inheritedAbstractSet.equals can also return true under one concurrent put that lands after its size
reads; the mirrored shape (physical size() prescreen, iterate this view, probe the argument'scontains, count == expectedSize) was evaluated and declined, since it reverses two adjudicated
rows and moves the weakKeys() false positive to the view. For exact comparisons, callcleanUp() with no concurrent operations; under variable expiration an entry that expired
within the current wheel tick stays until the next tick (see TimerWheel). An
additional asMap() size warning was declined as redundant with the cache's existing
approximation contract.
The async synchronous view has the same gap for in-flight futures, which its size() counts and
its iteration skips. cleanUp() cannot remove them, so the gap lasts until the load settles.
Receivers split by algorithm: AbstractMap's size check (HashMap, the bounded cache, the view
itself) makes the comparison false in both directions, while ConcurrentHashMap's two-sided scan,
which the unbounded cache delegates to, makes chm.equals(view) true and view.equals(chm) false.
No equality on the view is symmetric with both, and moving the unbounded cache to the size check
would leave ConcurrentHashMap itself asymmetric.
Maintenance nudges
Read paths nudge scheduleDrainBuffers() when they observe an expired/collected
entry (getIfPresent, containsKey, containsValue, getAllPresent, the iterator,
and the key/value/entry spliterators), so lazily-detected garbage is reclaimed promptly.
On a plain miss, getIfPresent still assists when the status is REQUIRED; getAllPresent
and membership reads have no corresponding absent-entry assist. The nudge is a cheap flag
check when maintenance is already running. On a caller-runs executor it runs maintenance inline,
so an in-progress scan of data.values() can have a node reaped underneath it — e.g.containsValue is an O(n) scan and the internal LocalCacheSubject validator calls it
per node while iterating data.values(); a weak key collected mid-scan is then
drained, correctly removing and killing a node the weakly-consistent iterator still
yields. Production readers tolerate a dead node (they check isAlive/getValue), and
the validator was made robust to it: it iterates data.entrySet() and validates a node
only if it is still mapped under its key, so a node reaped mid-scan is skipped while a
node genuinely stuck in the map (a leak) stays mapped and is still caught. Inside a
computation or eviction listener the same inline cycle is unsupported nesting; see
ConcurrentHashMap Constraints.
A failed replace nudges only when the entry is garbage. Both overloads signal "did not
update" by clearing ReplaceContext.oldValue, which alone cannot tell a dead or expired entry
from a caller's expected value that did not match. ReplaceContext.garbage separates the two so
that only the first schedules maintenance. "fix minor edge cases in put and remap" added the nudge
for the expired case but hung it on the shared signal, so replace(k, expected, new) also
submitted a task on every healthy CAS failure. Over 10⁶ failing replaces against a one-entry
cache, removing it took an unexpiring cache from 34 ms to 15 ms and an expireAfterWrite cache
from 33 ms to 28 ms. Both directions are pinned:AsMapTest.replaceConditionally_wrongOldValue_noMaintenance andExpirationTest.replaceConditionally_expired_maintenance.
The conditional overload tests hasExpired before containsValue, matching remove(k, v)'s
branch order, so an expired entry is reclaimed whether or not the caller's expectation held.
Testing containsValue first would let a healthy mismatch skip the ticker read and reach 16 ms
on the expiring cache, and it was rejected: the two conditional operations should read alike, and
the saving is one System.nanoTime() (9 ms per 10⁶ calls on the same machine). containsValue
cannot be tested before the null checks at all, since a cleared value reference fails it and the
entry would be misfiled as a mismatch.
Known JDK Interactions
StackOverflowError can leak the eviction lock. If user code causes aStackOverflowError inside a cache operation, the eviction lock can stay permanently held and
block all subsequent writes (JDK-8318888, open; JDK-8319309 is its duplicate). It happens at
release, when unlock() fails to run, and at acquisition: lock() and tryLock() are@ReservedStackAccess, so an overflow inside them completes the acquisition and raises the error
on return, before the caller's try. The canonical lock(); try { } finally { unlock(); } leaks
the same way, so no call-site change removes it.
PerformCleanupTask is not a ForkJoinTask. ForkJoinPool.execute(Runnable) does not wrap aForkJoinTask, and its source comment says "avoid re-wrap". The steal path claims a slot with a
CAS on the slot content, then writes the absolute value base = b + 1. A worker can stall between
its base recheck and the CAS. If the pool receives the same task object again, the CAS can
succeed on a later copy, one or more laps around the array. The worker then moves base back by a
multiple of the array length, and every slot is null. No code in the pool repairs this state.
JDK 22 and later reject every submission to that queue with Queue capacity exceeded (#2021).
JDK 21 and earlier silently lose or overwrite the tasks in that queue. From v2.2.4 to v3.3.0, the
task extended ForkJoinTask, and exec() returned false. This reuse avoided the per-submission
adapter, which was a profiling hotspot in 2016. The reuse also made a throw from exec()
permanent: run() catches every Throwable, so only its own logger.log could throw, on aStackOverflowError while logging or a logging backend that throws, and that completed the task
exceptionally so that a ForkJoinPool never ran it again, leaving maintenance to the write
buffer's inline assist and cleanUp(). A fresh wrapper per submission confines such a throw to
that submission.
A call to reinitialize() before each submission does not prevent the race. The race needs only
the same reference in the same slot, with any task status. A small set of rotated task objects
does not prevent it either. The fix is in the class and not in scheduleDrainBuffers, because the
pacer also submits the same object through Scheduler.forScheduledExecutorService.
The pool now allocates one RunnableExecuteAction for each submission. drainStatus coalesces the
submissions that cache operations trigger, and the pacer holds at most one delayed submission. The
two paths can overlap, so one cache can have two submissions in flight. On JDK 25.0.1,GetPutBenchmark and a weakKeys() copy ran with 3 forks and -prof gc. Every throughput change
was within the 99.9% confidence interval. Allocation increased by at most 0.06 B/op, and by 0.4
B/op for weakKeys() read/write. The 2016 result, that the adapter halved weakKeys() read
throughput, did not reproduce. Each bounded cache is 8 bytes smaller, because its task no longer
has the ForkJoinTask status field.
The task extends WeakReference rather than holding one, which saves an object per cache (16
bytes with compressed oops). Its clear() and enqueue() do nothing, so only the garbage
collector clears the referent. enqueue() clears without calling clear(), so both overrides are
needed. Without them, an executor that cleared the task it receives would make every later
execution a no-op. This is a deliberate exception to treating a contract-violating component as
the user's bug: the overrides cost nothing on any path, and the ForkJoinTask version guarded the
same way with no-op complete, completeExceptionally, and cancel. An executor can still halt
maintenance by never running the task. BoundedLocalCacheTest.cleanupTask_ignoreClear pins the
overrides.
Pacer
The Pacer rate-limits expiration maintenance scheduling. It usesTOLERANCE = ceilingPowerOfTwo(1 second) (~1.07s) as a minimum delay threshold,
preventing scheduling storms from rapid expirations.
nextFireTime = 0L is the unscheduled/cancelled sentinel. calculateSchedule
bumps any computed fire time that would equal 0L up to 1L to prevent a
collision with the sentinel — needed only in edge cases where now + TOLERANCE
or scheduleAt lands exactly on zero, but the guard removes the ambiguity for
readers of schedule()'s recursion check.
future == null && nextFireTime != 0L is a deliberate transient, not a wedge —
and its safety leans on GuardedScheduler. schedule() commits nextFireTime
(via calculateSchedule) before calling scheduler.schedule(), then publishes
the returned future. Between those two steps the pacer is momentarily in that
state, which the immediate-scheduler short-circuit relies on: an immediate scheduler
runs command synchronously inside schedule(), re-entering schedule() before thefuture is published, and the nextFireTime != 0L check breaks the recursion.
Reaching that state on the replacement path is what makes the short-circuit
complete, and it is why the reschedule arm calls cancel() rather thanfuture.cancel(...). cancel() clears nextFireTime and the field together, so a
re-entrant call meets a fully unscheduled pacer and takes the guard; cancelling the
future alone leaves the old one published, and then !future.isDone() is permanently
false for an immediate scheduler, so every re-entry cancels and reschedules without
bound. That was a live defect: a Scheduler running its command synchronously and
returning a completed future hung put on every executor including commonPool,
silently, because GuardedScheduler and PerformCleanupTask.run swallow theStackOverflowError and the stack immediately re-descends. Pinned byExpirationTest.schedule_immediate_completed; its neighbour schedule_immediate
returns an incomplete future, which is the shape a real inline scheduler never
produces and is what masked this. Ifscheduler.schedule() could throw on the first call, that same state would never
clear — every later schedule() early-returns and cancel() no-ops (future stays
null), permanently disabling prompt expiration. It can't: GuardedScheduler catches
every delegate throw and maps a null return to DisabledFuture. Caffeine.getScheduler
wraps a user-supplied scheduler this way, but passes the built-in systemScheduler()
and disabledScheduler() through unwrapped — Caffeine's own scheduler types are known
not to throw or return null, so only user types need guarding. Either wayscheduler.schedule() here never throws or returns null. The ordering is safe as written
— don't wrap it in a try/catch to "harden" an unreachable throw, and don't hand Pacer
an unguarded user scheduler (that, not the ordering, would be the bug).
SystemScheduler returns a future separate from its timer, so that a cancel removes the JDK's
delayed task. The timer is orTimeout on an internal future, and its timeout completes the
returned future and submits the task. Any completion of the returned future, a cancel included,
completes the timer normally, the one completion on which orTimeout's canceller removes the
delayed task on every JDK: on 11, and on 17 before 17.0.14, it ignores an exceptional completion
such as a cancel (JDK-8303742). delayedExecutor keeps no handle at all, so with it each
cancelled task stayed queued until its original fire time (896 B on JDK 25), one per cycle of a
cache emptying and refilling. SchedulerTest.systemScheduler_cancel pins the release; on JDK 11,
the default test JVM, a single future fails it. Revisit when the JDK baseline moves: from 21 one
future suffices (completeOnTimeout, with the task chained by thenRunAsync), and from 25ForkJoinPool.commonPool().schedule returns a cancellable future.
The system scheduler has three accepted consequences. A cache that is dropped leaves its
pending timer queued until it fires. The cache is held weakly and remains collectable, but a
process that churns caches with long expiries keeps about churn rate times remaining delay of
those timers. The timer submits the task from the JDK's shared delay thread, so with a direct
executor the maintenance cycle, eviction listener included, runs on that thread and delays every
other orTimeout, completeOnTimeout and delayedExecutor task in the JVM until it returns. A
rejection at fire time loses that cycle only: the returned future is complete before the
submission, so the next re-arm schedules normally, and the rejection is dropped in the timer's
discarded stage. The executor(Executor) javadoc warns that an executor "that discards tasks or
never runs them may experience non-deterministic behavior."
rescheduleCleanUpIfIncomplete piggybacks an already-scheduled pacer fire, by
design. Its immediate resubmission is limited to executor == ForkJoinPool.commonPool(),
since it cannot tell a transparent wrapper from a caller-runs executor, so a wrapper around the
common pool (for context propagation, say) takes the custom-executor arms: with a removal
listener under continuous eviction a forwarding wrapper measured 232.9 against 144.5 ns per
insert, against about 8 ns without one. A drainStatus == REQUIRED backlog re-arms the
pacer only when !pacer.isScheduled(); if a fire is already pending (the next expiration
event), the backlog rides that fire rather than stacking a second schedule. An expiration
backlog stays prompt regardless — a >EXPIRATION_THRESHOLD backlog leaves an
already-expired deque/wheel head, so getExpirationDelay returns ≤ 0 andexpireEntries already scheduled the pacer at TOLERANCE (~1s). Size eviction is
uncapped (drains fully in one cycle), so it never backlogs. A region transfer
(evictFromWindow, demoteFromMainProtected) is capped at QUEUE_TRANSFER_THRESHOLD and
re-arms after expireEntries armed the pacer, so a large window resize rides the pending fire
too, delaying the rebalance rather than overfilling the cache. A reference backlog defers
the same way a write-buffer one does, and the delay costs a late COLLECTED notification
rather than a stale read, while the entry's other half (a weak key's value, or a weak or soft
value's key, node and table slot) stays reachable until a cycle polls it. A GC schedules no
maintenance, cleanUp() polls at most REFERENCE_THRESHOLD + 1 references per queue per
call, and a cache with only reference collection has no pacer even with a Scheduler. The
shape is a write-buffer backlog (drainWriteBuffer's WRITE_BUFFER_MAX cap, reached only when
a concurrent writer refills during the drain, or its relaxedPoll passing over a slot a
producer has not published) on a cache whose next expiration is
distant, that then goes idle: the buffered policy tasks — LRU/weight bookkeeping over
CHM mappings that are already committed and visible — wait for that distant fire or
any later write / read-stripe / cleanUp. If the pass over an unpublished slot instead settlesIDLE (the accepted weak-memory strand) while no other entry is expiring, no fire is pending,
and only a later operation drains the task. Worst observable is a transient over-maximumSize on an idle cache, the documented async-eviction contract — plus, under
variable expiry, a deferred expiration notification: an entry whose AddTask is
still buffered is not yet in the timer wheel, so it was invisible to thegetExpirationDelay that armed the pending fire, and its removal listener waits for that
fire (which can exceed the entry's own TTL, since the armed time came from the
policy-visible entries) or the next cache operation. Reads stay correct throughout — lazyhasExpired gates every read. Best-effort amortized maintenance; don't drop the gate to
force a ~1s reschedule (it churns the distant fire's cancel+reschedule for a narrow,
self-healing transient).
Without a Scheduler, maintenance is amortized onto callers, by design. The immediate
re-arm in rescheduleCleanUpIfIncomplete is restricted to commonPool because any other
executor may run the submission on the calling thread, where the work is no longer
amortized and the caller pays a whole cycle. The deferred pacer arm above is the fallback
for a custom executor, and it needs a Scheduler. Configuring one requests prompt expiration;
a size-only cache has no pacer. Without this arm, a REQUIRED backlog waits for the next cache
operation, so a quiesced cache stays over maximumSize until then. The excess is capped,
not unbounded: the backlog is write-buffer tasks, MpscGrowableArrayQueue is bounded atWRITE_BUFFER_MAX, and a full buffer forces afterWrite's inline assist, so a quiesced
cache's estimatedSize() cannot exceed maximum + WRITE_BUFFER_MAX. Measured withmaximumSize(10), a single-thread executor and no scheduler, 200,000 writes then idle: 4
of 10 runs stayed over maximum, and the largest residue over 20 runs was 1,987 against a
2,058 bound. The live peak while writers run is higher. A drain applies up toWRITE_BUFFER_MAX + 1 insertions before evictEntries runs, producers refill the buffer
meanwhile, and each writer blocked in the inline assist has already committed its mapping, so
the peak is maximum + 2 * WRITE_BUFFER_MAX + 1 plus the writers. With WRITE_BUFFER_MAX at
2,048, 16 writers peaked 4,080-4,113 over maximums of 10 to 10,000 and 64 writers 4,161 over,
converging to the maximum when they stopped. The model is a garbage collector's: the excess is
capped and reclaimed on the next operation rather than on a timer. Don't add a third arm. A caller
cannot know whether the executor would run the submission inline, and moving it intoPerformCleanupTask, where a held eviction lock does identify a caller-runs execution after the
fact, fails on the same ground: prompt size eviction in an idle cache is not a contract the cache
offers.
Refresh Internals
The loader's original future is both refresh token and public result. Its identity
represents the generation. Per-generation copies would change refresh(k) / policy.refreshes
and stop cancellation reaching the loader's future. Any holder's completion reaches it the same
way: complete(v) is cached and counted as a load success, orTimeout fails every caller waiting
on that load and removes the entry, and completing a Policy.refreshes() future installs its value
and discards the loader's. Completion also precedes dependent
handlers, so awaiting that future cannot guarantee the cache-updating handler has run.
Reusing one pending future for two generations of the same key is outside this model: the
older handler can claim the successor's token and both can dispose of the produced value,
affecting a non-idempotent listener. Ordinary LIFO dependent completion makes the window
narrow; a coalescing loader can hand out copies itself. A write-time ownership condition
repairs neither double disposal nor orphaning across preserved timestamp changes.
Distinct future instances with custom equality are likewise unsupported. Map conditionalremove / replace and refreshes.remove use equality, which matches identity for standardCompletableFuture but can remove a successor for a hostile subclass. Repair would require
identity-conditional operations throughout both caches and the refresh registry, not a local
ownership check.
A manual synchronous-cache refresh uses one LocalLoadingCache.RefreshOperation per call.
It carries the registration and completion state, while the loader's original future remains
the token and return value. Registration runs inside refreshes.compute; completion is attached
after it returns, and coalescing onto an existing refresh attaches no additional handler.
The async view's absent-key branch registers with putIfAbsent rather than a compute and owes
the same rule: it attaches its by-key release only when it won the registration. Attaching that
release to whichever future it returns puts a refreshes.remove on a token this caller never
registered, and since dependents run most recently registered first it fires ahead of the owner's
commit, which then reads itself unowned and discards a reload no write had superseded. The
identity argument does not catch it, because the token removed is the owner's; only the caller is
wrong. A refresh completion that commits a mutation still discards the current registration by
key. Pinned by AsyncLoadingCacheTest.refresh_absent_racingRefresh.
refreshIfNeeded is intentionally lock-free. Reads of writeTime, getKey,getValue, getKeyReference, isAlive, and the CAS of writeTime happen
without synchronized(node). A stale observation could let asyncReload fire
on a just-retired node, but the completion-path ABA guards (currentValue == oldValue + (node.getWriteTime() & ~1L) == writeTime) discard the result. Cost
of the rare spurious loader call is accepted to keep the refresh fast path lock-free.
The write-time guard is itself immune to a same-instance overwrite reusing the old
value: a refresh-eligible entry's age exceeds refreshAfterWriteNanos, which makesexceedsWriteTimeTolerance true in both of its refresh arms (a duration within the
tolerance takes the always-true disjunct, a longer one is exceeded by the age), so an
intervening write always moves writeTime and the stale completion cannot match it.
That arithmetic is load-bearing; a change letting an update skip setWriteTime on a
refresh-eligible entry would re-open the stale-reload commit.
The low bit of writeTime is a soft-lock marker, and the completion ABA check
must mask it. A reader probing for a refresh CASes writeTime → writeTime | 1
while it registers the token in refreshes, then resets it; it starts no load if
the token already exists, so its transient marker is invisible to any stampede check.
The completion compares the base write time (& ~1L), not the raw value — a
concurrent reader's transient soft-lock is not a modification, and comparing the raw
value discards a perfectly good reload (issue #1970). The completion also keeps the
token registered in refreshes across the value swap: it reads the token to confirm
ownership rather than removing it, and the compute machinery's discardRefresh clears
it after setWriteTime. Holding it for the swap keeps concurrent reads debounced so
none can trigger a stampede in the remove-then-refresh window. Finally, thecomputeIfAbsent lambda re-validates the marker under its per-key atomicity and aborts
when a concurrent refresh already completed (its setWriteTime cleared our marker),
so a delayed reader that passed the containsKey gate does not launch a duplicate,
stale reload from the same prior value. On the absent-create path the token clear runs
in a finally, so a weigher or expiry callback that throws while creating the entry
cannot orphan the token (#1970).
discardRefresh is deliberately over-aggressive. A mutation that races a
refresh discards whatever token is in refreshes without trying to prove it's
the same generation. A refresh that registered before the mutation published its value was
launched against the old value, so killing it is correct for linearizability. Insertions and
unbounded writes release inside the map computation, before the new mapping is visible, so they
discard only such refreshes. A bounded update of an existing entry (put, both replace
methods, and remap's present-entry write) publishes the value before its release, so a refresh
registered in that gap loaded the new value and is discarded as well. That over-discard is
accepted: the successor's reload is declined and the next refresh loads again.
A refresh completion's mutating exits follow the same rule, and releasing only the completing
token was rejected. A commit that replaces, creates, or removes the entry is a mutation, and a
manual refresh(k) may replace its completed token while it runs. Releasing only the completing
token rescues a successor that loaded the published value, which only a bounded update exposes,
but keeps one that loaded the replaced value and can never install: its registration outlives the
commit, so a later refresh(k) coalesces onto its load and is discarded with it, andrefreshIfNeeded's containsKey gate suppresses automatic refresh until that load finishes. That
is the residue the prescreen race below leaves, widened to any refresh that registers during a
commit before its value is published. In aligned races of 10,000 trials per case, owner-conditional
release removed 31 discarded successors when nothing followed and added 147 discarded later
refreshes when one did. Pinned byRefreshAfterWriteTest.refresh_committingCompletion_discardsStaleSuccessor and itsrefreshIfNeeded and absent-create twins, which park the commit in its Weigher.
The bounded cache's containsKey prescreen stays, and the race it leaves open is
benign (accepted). ConcurrentHashMap.remove takes the bin lock
whether or not the key is there, so the prescreen is what keeps an ordinary write
off that lock. It was added to the bounded cache only; the unbounded cache still
removes unconditionally. What the prescreen cannot see is an in-progresscomputeIfAbsent reservation, which reads as absent, so a write landing inside
that window leaves a token launched from the generation it superseded. On the
automatic path nothing is committed wrongly: the completion's ABA guards reject
the value, and the registration re-validates node.getWriteTime() inside the
reservation, which rejects any write that lands earlier. That first clause is load-bearing and was
only true by accident until 2026-08-24. The completion's write-time term reads
the node captured when the reload started, and retire()/die() leavewriteTime frozen, so a remove and reinsert of the same value instance
presented an unchanged value and an unchanged write time and the guard committed
a reload launched from the dead generation. The commit branch now also requiresnode.isAlive(), which is exactly the generation test the other two terms cannot
make; both remain necessary, since the write time still catches an in-place
update of a node that never died. Pinned byBoundedLocalCacheTest.refreshIfNeeded_reinsertedNode_rejectsStaleReload. There the residue is
bounded by one load and is a delay rather than a loss: automatic refresh is
suppressed for that key while the orphan is in flight (refreshIfNeeded gates on
the same containsKey), and a refresh(k) issued after the write coalesces onto
the superseded token and is discarded. Don't drop the prescreen to close it, and
don't add a registration-side re-check: closing it there would fix onlyrefreshIfNeeded and leave the manual LocalLoadingCache andLocalAsyncLoadingCache registrations, which carry no write-time marker to test,
while implying all three were closed.
A manual refresh that started absent can commit across the same window, and that is
accepted. LocalLoadingCache.RefreshOperation has no node and no write time, so its only
ownership test is currentValue == oldValue, which is null == null when the key was absent
at registration. A put followed by an invalidate inside the reservation window therefore
leaves the token registered and lets the completion install its loaded value over both.
Measured on the bounded cache with a loader parked inside the registration, 5/5 per arm: put
then invalidate installs the load, put alone leaves the written value (so the guard works
wherever it can tell the two apart), and a present-start refresh is discarded. A present start
whose racing writes end on the instance it read (put(k, 6); put(k, 5) after reading 5) installs
over them, since the identity test cannot tell that from no write. The escape needs the loader to
run inside refreshes.compute, which means a direct executor, a saturated pool withCallerRunsPolicy, or an AsyncCacheLoader returning a completed future; onForkJoinPool.commonPool() the registration publishes first and the refresh is discarded 0/5.
The resulting history is still a legal linearization (put, invalidate, then the refresh's
install), and no public contract covers it: Cache.invalidate is undefined only for an entry
being loaded and otherwise not present, which the put makes false. The asynchronous cache does
not share the shape, since its absent case installs an in-flight future throughcomputeIfAbsent on data and registers with putIfAbsent.
The unbounded cache keeps the unconditional remove, so a write there waits out an inline
refresh load on the same key (measured: a 2004 ms put against a 2 s load). Accepted.UnboundedLocalCache is a light ConcurrentHashMap wrapper, and CHM's put is pessimistic
anyway, so there is no optimization proper to apply there. The bounded cache takes the
complexity because it already coordinates a great deal of metadata for speed.
A cleared reference is equal only to itself. InternalReference's equals compares referents,
so two distinct references that have both been cleared used to compare equal (null == null) and
aliased whenever their cached System.identityHashCode values collided. Nothing in production
depends on that: every lookup that can involve a cleared reference (drainKeyReferences,drainValueReferences, evictEntry) passes the same object, which the identity short-circuit
already answers. referenceEquals and objectEquals therefore require a live referent. The one
property this drops is cross-type null equality between a WeakValueReference and aSoftValueReference, which cannot arise since a cache is one or the other; ReferenceTest .reference_equality now asserts each cleared reference forms its own equality group.
Automatic refresh uses > where expiration uses >=, and that stays. refreshIfNeeded tests(now - writeTime) > refreshAfterWriteNanos() while hasExpired tests >= on all three of its
predicates, so with equal durations an entry is expired one nanosecond before it is
refresh-eligible. The > matches Guava and is unobservable on a real ticker (only a FakeTicker
advanced by exactly the duration can see it). Ruled 2026-08-24: the javadoc's "once a fixed
duration has elapsed" is not wrong enough to reword, and other Guava-compatibility choices rank
ahead of this one. Don't re-raise the boundary or the asymmetry.
All three refresh registrations key refreshes by referenceKey(key), never by the node's own
key reference. Under weak keys a node's retire()/die() calls clear() on the veryWeakKeyReference the node holds, and InternalReference.equals compares referents by identity,
so a cleared reference is equal to no reference built later for the same live key. A token
registered under the node's reference and preserved past retire() (the preserveRefresh exits
do exactly that) is then unreachable: the successor's own discardRefresh builds a fresh
reference and cannot match it, and Policy.refreshes() skips it because its referent is null, so
the registration and its future are retained for the cache's lifetime and the growth is invisible
through the public view. LookupKeyReference holds the key strongly and is never cleared, which
is why the two manual paths always used it; refreshIfNeeded now does too. This costs no extra
retention: the loader call and the completion closure both capture the key strongly for the
future's lifetime. The containsKey prescreen still reads node.getKeyReference(), which is
free and equal to the lookup key while the key is live, so the allocation lands once per
registration rather than on every refresh-eligible read. Don't key refreshes by anything the
node owns. Pinned byBoundedLocalCacheTest.refreshIfNeeded_weakKeys_preservedTokenIsDiscardable.
A refresh completion releases its own token in its catch, not only through remap. The
completion's commit normally clears the registration inside remap, so the outer catch looks
redundant. It is not: remap has throw sites that precede every discardRefresh, includingrequireIsAlive's broken-equals check and the ticker read behind hasExpired. A throw there leaves
the registration behind, and because refreshIfNeeded gates on refreshes.containsKey, that key's
automatic refresh is suppressed for the rest of the cache's life from one transient
user-component failure. All three completions therefore mirror their own error branch with the
identity-conditional refreshes.remove(keyReference, ownFuture), which cannot take a successor's
token. Note the throw sites after the discard are already safe, since the material tail discards
before returning from the map computation. Pinned byBoundedLocalCacheTest.refreshIfNeeded_completionThrows_releasesToken and its refresh /refreshAsync twins, which fail a refresh completion through a throwing Ticker.
A rejected reload is notified even though it was never in the cache. When the
completion's compute declines to install the reloaded value it sets a cause and callsnotifyRemoval(key, value, cause) — EXPLICIT on the absent exit, REPLACED on the reject
exit (a result identical to the current value is not notified). So a RemovalListener can see a
value that was never a mapping. That is intentional and follows from linearizability: the value
was produced, the cache decided not to keep it, and the listener is the disposal hook, so not notifying
would be the surprise — the value would be dropped with no chance to release what it holds.
Deliberately not spelled out in the public javadoc: the surrounding refresh ordering is
vague there (Guava was not linearizable either), and pinning this corner would over-specify it.
Don't "fix" the notification away, and don't treat the two causes as interchangeable.
Returning the captured oldValue still offers it as the new value to retain. If it is no
longer current, rejecting the refresh rejects that attempted resurrection and notifies again;
the earlier removal does not discharge disposal of this new offer. Do not suppress by comparing
with the captured old argument. Disposal after a user Weigher or Expiry throws during
installation is outside the cache's responsibilities; keep the existing failure cleanup.
The one exception is an internal query-style no-op, flagged with RemapHints.preserveRefresh:putIfAbsent on a present key, a non-matching conditional remove/replace, or an async-view
retry that returns an unfinished future without invoking the user's remapping function. Public
remapping functions cannot set these hints; returning the same user value is still a write.
These internal no-op paths leave a racing refresh intact. BothBoundedLocalCache.remap and UnboundedLocalCache.remap honor the hint (a same-instance
return with preserveRefresh set skips discardRefresh); a real mutation still discards.
The unbounded cache used to drop the hint and cancel the reload — the sibling caches must
stay in sync here.
The same hint also owner-scopes a refresh completion. When a refresh finishes it
re-enters compute to install or reject the reloaded value. Because refreshes holds one
future per key, a by-key discardRefresh from that completion is safe only while the
completing refresh still owns the registration. If a newer refresh has since registered — the
prior token was cleared by a racing write, or by an invalidate + refresh that re-registers
an asyncLoad on the now-absent key — the by-key discard would steal the successor's token,
dropping its freshly loaded value and leaving the cache stale (self-heals only on the next
refresh-eligible read). So each completion path (LocalLoadingCache.refresh,LocalAsyncLoadingCache.tryComputeRefresh, BoundedLocalCache.refreshIfNeeded) computesowned = refreshes.get(kr) == ownFuture and sets preserveRefresh = !owned on its non-commit
exits — reject and absent — mirroring the error path, which was already owner-scoped
(refreshes.remove(kr, ownFuture)). The hint protects a successor already present at the
ownership check. A successor that registers between that check and the by-key no-op release
can still be discarded; tightening this residual was declined. A later refresh recovers, and
this interval does not justify restoring the rejected token-bearing hint.
Honoring the hint therefore extends beyond the
same-instance no-op block: remap's two absent null-return exits (n == null and the
evicted-retire) and the unbounded absent exit skip the discard when preserveRefresh is set.
The absent-create exit does not, and must not: installing a value is a mutation, so the
over-aggressive-discard doctrine applies to it like any other write. Both manual paths create only
in their owned branch, but a successor can replace the completed token before the finally runs;
that successor loaded the key as absent, so discarding it is the doctrine rather than a steal.
Every one of the twelve callers that sets preserveRefresh either returns null or returns the
existing value of a present entry, so the exit is not reachable with the hint set. Pinned byBoundedLocalCacheTest.remap_absentCreate_discardsPendingRefresh andRefreshAfterWriteTest.refresh_committingAbsentCompletion_discardsStaleSuccessor.
The !computeIfAbsent evicted-retire exit is outside the rule for a second reason: it runs
before the remapping function, and the remapping function is the only thing that ever assignspreserveRefresh, so no hint exists yet whatever the caller passed (computeIfPresent andreplaceAll, the only non-creating callers, pass none). Reaping a dead entry for a caller that
may not recreate it is a purge, so it discards, the same reading as invalidate/clear below.
Pinned by BoundedLocalCacheTest.remap_evictedRetire_nonCreating_discardsPendingRefresh.
On a reject exit the hint cannot stand alone. remap honors preserveRefresh for a
same-instance return only at its preserveTimestamps no-op exit, so all three completion paths
set preserveTimestamps unconditionally there. refreshIfNeeded used to set it only when the
value or write time had changed, which left the one sub-case owner-scoping exists for
(registration superseded, entry untouched) falling through to the material tail. Both halves of
the fix then failed together: the tail's by-key discard stole the successor's token, and itssetWriteTime moved the write time the successor's own ABA guard tests, so the successor's fresh
value was dropped too and expireAfterWrite was extended by the stale reload's in-flight
duration. Gating the tail's discard on preserveRefresh fixes only the first half, which is why
the bounded cache does not carry that gate; UnboundedLocalCache.remap does, because it has no
no-op exit to route to. There is no debounce lost: a write that discards a token moves the write
time anyway, and a superseded token blocks re-arming through refreshes.containsKey. Pinned byRefreshAfterWriteTest.refreshIfNeeded_staleAfterReinsert_preservesEntry, which reaches the case
through a remove and reinsert of the same value instance, since a dead node's write time is
frozen and the completion's ABA guard therefore sees no change. A rejected completion now leaves
the entry refresh-eligible, so the next read arms a fresh reload instead of waiting out another
interval; BoundedLocalCacheTest.refreshIfNeeded_skip_discarded reads quietly for that reason.
The absent-branch steal is reachable in sync mode only: a successor refresh(k) on an
absent key registers an asyncLoad without inserting the entry, so the stale completion observes the entry absent;
in async mode the successor's get inserts an in-flight future, making the entry present so the
completion takes the reject branch instead. Don't reintroduce an unconditional by-key discard on
a non-mutating completion exit; a mutating exit keeps it (see the over-aggressive discard above).
The same sibling-sync covers a vanished-key skip: a non-creating caller (replaceAll,computeIfPresent) whose key was concurrently removed hits remap with value == null and
returns null — a no-op, not a mutation, so it must not discard a refresh registered
after the removal (that refresh raced nothing). Both caches thread a computeIfAbsent flag
(false for replaceAll/computeIfPresent, true for compute/merge) and return early
without discarding on the absent branch when creation is disallowed. A creating caller
(compute/merge) that returns null on an absent key still discards (over-aggressive, as
above). UnboundedLocalCache.remap used to discard unconditionally on the absent+null path,
diverging from BoundedLocalCache on the replaceAll-races-remove race. The async views routecomputeIfPresent, and the synchronous view's conditional writes when the key vanishes between
their quiet prescreen and the compute, through the creating compute, so they discard an absent
key's registration. That is harmless: an async registration for an absent key can only be an
orphan (the optimistic refresh inserts before registering, and eviction and expiry discard with
the entry), so a later refresh(k) starts a fresh load instead of joining it.
invalidate(k)/clear() discard an absent key's pending refresh — a purge is not a
query. A refresh(k) on an absent key registers a reload in refreshes with no data-map
node (sync asyncLoad only), so a remove/clear that reaches discardRefresh only through
a present-node lambda leaves the registration alive; its completion then commits (owned still
holds, null == oldValue[0]) and resurrects the key past the purge — sync-only, since an
async refresh-of-absent inserts a physical in-flight entry that remove does see.
The fix keeps remove(Object) on data.compute (not computeIfPresent) so an absent key still
enters the lambda under the bin lock and discards there — deliberately, because the refresh
completion commits under that same bin lock, so doing the discard outside it (after acomputeIfPresent miss) races: the completion can insert between the absence check and the
discard. clear() purges the whole refreshes map up front for the same reason (the node loop
can't reach node-less registrations). This is the over-aggressive-discard doctrine applied to a
purge, which is legitimate exactly as it is for present keys. Note remove(k, v) is not
extended: a conditional remove that matched nothing (absent or wrong value) is a query-style
no-op that "raced nothing," so it preserves the refresh (like remove(k, wrongValue) on a
present key). The traversal-based bulk removals (removeIf and retainAll on the key, value
and entry views) and computeIfPresent also preserve the node-less registration: the key is not
an element of any view, so they match nothing for it, and only a purge (remove(k),invalidate, invalidateAll, clear(), compute -> null) discards it. Don't move the
absent-key discard back outside the bin lock.
A sync refresh(k) on an absent key is an isolated side-load; the async view makes it a
first-class in-flight entry — a structurally-forced divergence, not a bug.LocalLoadingCache.refresh on an absent key registers asyncLoad only in refreshes() and
leaves the data map untouched until the completion compute inserts. So the pending reload is
invisible to a concurrent get(k), whose computeIfAbsent sees an absent map and loads again
— two loader invocations, and the refresh's value is then discarded with a phantom REPLACED
notification carrying a value that was never a mapping. The async view
(LoadingCacheView.tryOptimisticRefresh) instead side-loads via asyncCache.get(...), inserting
the in-flight future into the data map immediately, so a concurrent get joins it (one load) and
the future is joinable / invalidatable / cancellable. This is inherent: a sync cache cannot join
an in-progress computeIfAbsent from outside the bin lock, so it cannot dedup a refresh against aget the way the async view (whose loads are visible cache entries) can. The LoadingCache.refresh
javadoc — "Returns an existing future without doing anything if another thread is currently loading
the value for {@code key}" — holds under the narrow reading (another refresh in flight; both
siblings dedup via the refreshes map) but not the broad reading (any load, including aget-initiated one); that scope gap is intentional and left as-is. Cancellation blast radius
differs accordingly: cancelling the future from async refresh(absentKey) cancels the shared
in-cache load (all get waiters get CancellationException, handleCompletion removes the entry),
while cancelling the sync refresh future only unregisters the isolated reload and leaves a
concurrent get's own load untouched. An expired entry that maintenance has not yet removed
takes the same absent route in both caches: if maintenance removes it before the load completes,
that removal discards the sync cache's registration, so the loaded value is notified as EXPLICIT
while refresh still returns it, whereas the async view replaced the expired entry with its
in-flight load when the refresh began and installs the value. Don't try to make the sync cache
dedup a refresh against a get; pinned by RefreshAfterWriteTest.refresh_absent_sideLoad_*.
Sync getAll discards an unloaded key's refresh only on the sequential path — by design,
and it reflects a real consistency difference, not a bug. For a key that fails to load
while a refresh is in flight (an absent key whose refresh is still pending): the sequential
path (loadSequentially, no loadAll override) loads via get(key) → computeIfAbsent, so a
null load discards the refresh; the bulk path (an overridden loadAll) loads vialoadAll outside any lock and side-loads the results with put, so an omitted key never
reaches compute and its refresh survives. This is not an oversight to unify. A singleget(k) that loads null discards (same computeIfAbsent), and sequential getAll is exactly
N linearized get(k) calls — each observes absence atomically under the bin lock, which is
the standing that justifies discarding a racing refresh. The bulk path has no such standing:loadAll cannot run under a lock (CHM won't let us hold a bin across the load, and we can't
lock entries), so it is a non-linearizable side-load — closer to refreshAll than to a
load. Its only linearized moments are the put insert/replace points (where we match Guava,
atomic or not); the "this key was absent" observations happen outside any lock and a key may
materialize afterward (we stomp the still-missing keys but do not remove ones that appeared).
So a put, invalidate or clear() issued while loadAll runs is overridden when its results
are stored, re-caching what the loader read, where a per-key load makes the writer wait on its bin.
With no atomic absence-observation instant, it has nothing to hang a discard on. So the
sequential path discards because it can judge; the bulk path preserves because it can't —
forcing bulk to discard would impose an absence-decision onto the one path structurally unable
to make one. Don't "fix" the split; don't add discardRefresh to the LocalCache interface
for it. The asynchronous bulk load does not stomp a write to a requested key that lands during the
load: fillProxies completes the proxy the write displaced, so the caller receives the loaded
value, the cache keeps the write, and the write's REPLACED notification carries the loaded value.
Unrequested keys it returns are stored with put and do stomp such a write, or another caller's
in-flight load of that key, whose waiters keep their own result.
doComputeIfAbsent's new-node path preserves a racing refresh on a weigher/expiry throw —
by design; don't add a discardRefresh there. It discards on a clean value return (a real
mutation) and on a clean null return (the loader's authoritative "no value" verdict, same as a
single get(k) that loads null), but a weigher.weigh/expireAfterCreate throw aborts the
creation without installing anything, so it is not wrapped in a token-clearing finally. The
rule is coherent: a clean completion (value or null) is an authoritative verdict that discards a
racing refresh; a throw is an aborted op with no verdict, and an independent in-flight refresh
may still legitimately populate the absent key, so it is preserved. This is asymmetric withremap's create branch, which does discard on a throw (try {…} finally { discardRefresh },
the #1970 fix) — but only because remap doubles as the refresh-completion path and must
self-clean its own token on every exit; it can't tell a user compute from a completion, so it
discards uniformly (a doctrine-safe over-discard). That uniform discard is scoped to the exits a
completion can reach — the present-entry catch and the create-branch finally; the
absent-branch user-function throw precedes any materialization and no completion throws there,
so both siblings preserve it. UnboundedLocalCache.remap's catch used to discard on that path
too (a blanket catch (Throwable) from the "unbounded compute throws" fix whose intent was the
present-entry parity); it was narrowed to if (value != null) to match BoundedLocalCache,
which never had an absent-branch catch. doComputeIfAbsent is only ever a user load,
so it can afford the correct behavior. Adding the finally here would make a failedcomputeIfAbsent also abort an unrelated legitimate refresh — the inverted direction. Pinned byBoundedLocalCacheTest.computeIfAbsent_absent_weigherThrows_keepsRefresh (extends the
already-adjudicated "mapping-function throw doesn't discard is correct" to the weigher/expiry
throw).
Quick reference — discardRefresh across the compute family (verified consistent, both
siblings). compute/merge/computeIfPresent/replaceAll all route through remap;computeIfAbsent uses doComputeIfAbsent (BLC) or a direct data.computeIfAbsent lambda (ULC).
| Verdict | remap (compute · merge · computeIfPresent · replaceAll) |
computeIfAbsent |
|---|---|---|
| clean value (mutation) | discard | discard |
| clean null — present (removal) | discard | discard |
| clean null — absent (no-value verdict) | discard | discard |
same-instance no-op (preserveRefresh) |
preserve | preserve (returns existing) |
| user-function throw — present | discard | discard |
| user-function throw — absent | preserve | preserve |
| weigher/expiry throw — absent create | discard | preserve |
Cross-cutting rules layered on top: non-creating callers (computeIfPresent/replaceAll) on a
vanished key take the computeIfAbsent=false early return and preserve; refresh completions and
async-view no-ops set preserveRefresh so a stale completion can't steal a successor's registration
(owner-scoping); invalidate/clear discard an absent key's refresh (a purge, not a query). The
only intentional split is the last row — remap doubles as the completion path and self-cleans,doComputeIfAbsent is a user-load-only path (and ULC, having no weigher/expiry, has no such case at
all). The unbounded remap catch is guarded by value != null, preserving an absent
user-function throw like the bounded implementation.
The jcache adapter deliberately does not get this narrowing: RemapHints does not cross the
package boundary, so the adapter's query-style operations (a failed putIfAbsent, a NONE-action
entry processor) do discard an in-flight refresh and reset the write time. Adjudicated won't-fix —
see .claude/rules/jcache-adapter.md.
Async Synchronous View
AsyncCache.synchronous().asMap() queries are logical, mutations are
physical. containsKey, get, iteration, and containsValue treat in-flight
entries as absent (Async.isReady / Async.getIfReady). But KeySet.remove,removeAll, removeIf, retainAll, and EntryIterator.remove operate on the
raw delegate map without blocking on in-flight futures. Blocking everywhere
would invite deadlock, and unlike values()'s removals and entrySet().removeIf/retainAll,
which skip an in-flight mapping, these discard it. That is deliberate: the view is best effort,
and it is better to discard too much than to keep stale contents on linearization assumptions
the async cache cannot support. keySet().contains(k) != keySet().remove(k)
on a loading entry is accepted. size()/isEmpty() are physical too — they delegate straight to
the backing map and count in-flight entries.
Value-conditional CAS ops are the exception to "raw", and it is CHM-faithful. remove(k,v),replace(k,v), replace(k,old,new) and the compute/merge family block on an in-flight
future (resolved outside the compute, then CAS inside), mirroring CHM where a mutation must
take the bin lock an in-flight compute holds: in-flight is absent to reads but blocks
mutations as if present. Only the key-based removals above are raw. Bulk collection-view ops
split along the same line: values().removeAll/remove/retainAll are value-searches that skip
in-flight via the getIfReady filter (like CHM ValuesView.remove), so they do not block on a
mapping in flight when scanned (one replaced by a pending future between the scan and its
conditional remove(k, v) still waits in remapWhenSettled), whileentrySet().removeAll's iterate-argument branch routes through the blocking remove(k,v) (like
CHM EntrySetView.remove → map.remove(k,v) → bin lock) so it can. It iterates the argument only
when the keys are strong and the argument is not a Set larger than the physical size(), which
counts in-flight entries, so whether it waits for and removes an in-flight entry depends on how many
other loads are in flight. Don't "fix" that asymmetry —
and a test on the blocking path must coordinate threads (complete the future off-thread), as the_async conditional remove/replace tests do.
A logical read can return the value of a superseded future — the synchronous
view is not linearizable. Each read is a two-step composite (map read, thenAsync.getIfReady unwrap) with no re-validation between. If the future found in
step one is in-flight, is superseded by a concurrent put/invalidate, and then
completes, the resumed unwrap returns that future's value even though it was the
mapping at no instant in the reader's window (the completion's identity-conditionalreplace/remove no-ops once the future is unmapped). Only the rawAsyncCache.asMap() view over the futures is linearizable; the synchronous view
reads "the future it found." Double-collecting (re-reading the mapping after the
unwrap and returning null on change) would close it but adds a map re-read to every
sync-view hit for a narrow, non-linearizable-by-design corner. Don't add the
re-check guard.
The same two-step shape is AsMapView.computeIfAbsent's optimistic prior-future
branch, the one exit in its retry loop that does not re-read the mapping. A future
removed and completed while the caller sits between the map read and the isDone
test yields that future's value with no mapper call, where an unpaused caller would
have waited on the pending future, looped to getIfPresentQuietly, found the key
gone, and computed. Same adjudication: the value came from a future that was the
mapping when it was read, an in-flight mapping counts as present forcomputeIfAbsent under the coalescing rule below, and ConcurrentHashMap promises
nothing about a mapping surviving the return either.
The compute variants adopt the found future too — synchronous().get(k, func) /getAll load-coalesce, they do not recompute. The present-branch ofLocalAsyncCache.get returns any in-flight future it finds (no isDone/isReady
screen) and AbstractCacheView.resolve joins it, so the caller's function never runs
and the future's outcome is adopted — a null result, a rethrown foreign load
exception, a raw CancellationException (resolve doesn't unwrap it), or an
indefinite wait on a stuck future. This diverges from sync Cache.get(k, func) (=computeIfAbsent, the ConcurrentHashMap family, which recomputes when the prior load
left no mapping) and from the same view's asMap().computeIfAbsent (whose retry loop
recomputes on null/exception/cancel). That is not a bug: Cache.get/getAll promise
neither model, load-coalescing is a legitimate policy (Guava's LoadingCache coalesces;
CHM does not), and the synchronous view cannot fully emulate the async view's
linearizability regardless — it "reads the future it found" for computes as well as
reads. Whether coalescing is better or worse is perspective-dependent; the point is only
that it differs. Don't "fix" get(k, func) by routing it throughAsyncAsMapView.computeIfAbsent's retry loop.
The adopted outcome includes the caller's own failure. A failed future stays mapped until its
completion's dependent logs the failure and removes it, and the caller's join can return while
the completing thread is still in that dependent. An immediate retry, through the view or from a
dependent on the future, then rethrows the same exception without calling its function, in 85-99%
of immediate view retries on defaults. Computations are not special here: a future passed to put
that fails is visible to the same reads until its removal runs, which is why the public javadoc
does not name a failed computation's window. Removing the failure in the view before rethrowing
was declined: it helps only that caller's retry, since any other call in the window still finds
the future, and "the mapping is left unestablished" holds once the removal runs.
Read bookkeeping happens only at the lookup that finds a ready value. asMap().putIfAbsent
and computeIfAbsent record the access (access time, read expiry, frequency) on their first
lookup. A value they obtain by waiting on a load, or that their atomic step finds after that lookup
missed, is returned without it, so a variable Expiry sees no expireAfterRead until the next
read. Waiting matches Cache.get(k, func) and the future-typed API, neither of which applies read
expiry to an in-flight future. The raced exit is the one place a ready value skips the read
(Cache.get goes through computeIfAbsent, which reads the entry it finds); covering it needs a
second map read or a remap hint honoured by both caches, for a window between two steps.
A write or read in the finalization window is accounted as meeting an in-flight load. The
view's caller has the loaded value once the future completes, but handleCompletion's quiet
finalization runs in a dependent that can follow it, and until then the node carries the async
sentinel. A put there is dated by expireAfterCreate rather than expireAfterUpdate, a read'sexpireAfterRead is dropped, and the loaded value's own creation is then skipped or applied over
the read. Only which Expiry leg dates the entry differs; the mapping, listener and weight are
correct. Telling a delivered value from an in-flight one needs a readiness probe on every write
and read.
In the same window the age and remaining-time accessors (ageOf, getExpiresAfter(key)) return
empty and every weight surface reports 0, while the value reads, getEntryIfPresentQuietly and
the snapshots report the entry present with its current deadline, which under variable expiry is
the insertion time plus ASYNC_EXPIRY. Both are deliberate: the entry cannot expire before the
handler runs, and its creation expiry has not been computed; hiding the entry would makegetEntryIfPresentQuietly disagree with getIfPresentQuietly, and a clamp would still report
about 146 years. Pinned by the ageOf_async_pendingCompletion tests andExpireAfterVarTest.getExpiresAfter_async_pendingCompletion.
Async Put Re-registration
AsyncCache.put(k, future) re-registers a completion handler whenever the prior
mapping differs by identity, so re-inserting an already-registered future
double-fires handleCompletion. The dedup in LocalAsyncCache.put only skips a
consecutive same-instance put (prior == castedFuture), so put(k, f1); put(k, f2); put(k, f1) leaves two whenComplete handlers on f1. A single
completion then replays replace + recordLoadSuccess once per handler. Accepted,
because there is no correct dedup: we cannot inspect a future's already-registered
dependent actions, and tracking our own registration history would be wrong (unbounded,
and stale the moment the entry is replaced). It is benign regardless. Re-inserting a
specific future instance after replacing it is unusual, and the secondreplace(k, f1, f1) is idempotent: notifyOnReplace suppresses on identity, and the
quiet write preserves the expiration because the first handler already cleared the async
sentinel, so the user's Expiry sees one creation rather than a creation and an update.
The load is still counted once per handler. Pinned byAsyncCacheTest.put_reregisteredInstance_completionRegisteredTwice.
Failure cleanup is scoped to the future, not to each insertion. A deferred finalizer that
fails its weigher or expiry removes (key, future) conditionally. If the same completed future
was removed and reinserted meanwhile, that cleanup can remove it again: reinsertion does not
create a new computation or cleanup owner. A distinct successor survives, as pinned byAsyncCacheTest.handleCompletion_weigherFails_preservesConcurrentReplacement and its getAll
twin. Do not add an insertion-generation token for the same-future case. Repurposing that future
with obtrudeValue does not establish an independent computation lifetime either; use a distinct
future for that. This does not change the intentional bulk-proxy obtrusion below.
A cancelled bulk proxy stays mapped, and the completer re-asserts its lifecycle.getAll installs a proxy per absent key and gives its lifecycle to AsyncBulkCompleter
rather than to handleCompletion, so a caller who cancels the mapped proxy leaves the
entry in place. Measured: the entry stays mapped and counted while queries filter it, aget in that window adopts the cancelled future instead of starting a fresh load, and
once the load settles fillProxies obtrudes the value onto that same future, leaving the
entry holding it and the proxy no longer cancelled. The single-key path differs because
its handleCompletion removes an entry whose future completed without a value, so
cancelling there is self-healing.
Accepted 2026-08-16 as the least bad of unsatisfactory options, and the reasoning is what to
re-read before proposing a change. Cancellation does not stop the computation, so the value
still materializes, and a cache that dropped the entry on cancel would have no way to hand that
value to a removal listener for cleanup, where an entry removed while in flight does have a
listener attached to notify. A later refresh(key), invalidate(key), or synchronous-view
write such as putIfAbsent or compute still treats the cancelled proxy as a completed
valueless mapping and replaces or removes it, so in that sequence the bulk value reaches
neither the cache nor the listener, as after a per-key cancel. Cancelling says downstream
chained actions may be abandoned, which still happens; removing the mapping directly says the
value is not cacheable. Obtruding is then the cache treating the value as having materialized
elsewhere and re-asserting its own lifecycle over it. Removal-on-cancel is not free either: the
cancellation happens outside the cache, so it takes a dependent action per proxy, and it would
leave the completer's replace unable to install the loaded value. CompletableFuture offers
no API that satisfies every case and there is no clean decision matrix, so don't add
cancel-aware completion logic to the bulk path, and don't screen the cancelled future out ofLocalAsyncCache.get's present branch, which the adopt-the-found-future decision covers.
TimerWheel
Wheel resolution is one level-0 tick: 2^30 ns (~1.074s). An advance within a tick
(for example 0 → 1) yields delta = 0; crossing -1 → 0 after the wrap-bias repair crosses
a rebased boundary and yields 1. Hashed wheels process whole ticks to retain O(1) insertion
and deletion; see research foundations on timing wheels.
Queries filter an expired entry immediately, but physical removal and its notification can
wait until the next tick. On a fake ticker, expiries of 1ns, 1ms, and 500ms stayed resident
after cleanUp; setExpiresAfter(k, 0) was reaped at 2^30, not 2^30 - 1.
The bound is one tick, not a whole wheel revolution: expire scans [start, start + delta]
inclusively (steps = 1 + delta) and checks each node's deadline. getIfPresent andcontainsKey filter without removing; the compute path (get(k, fn) through remap) reaps.
Fixed expiration is more eager because its deque has no tick boundary to wait for.
That bound assumes a deadline that has not moved since the node was scheduled. A
variable-expiry read applies its new duration through tryExpireAfterRead's CAS and leaves the
matching reschedule to onAccess, which runs only when the read buffer accepted the node. A
dropped offer leaves the node in the bucket its previous deadline chose, so a shortened deadline
waits for that bucket's sweep. Measured with an Expiry of 1h on create and 2s on read, 400 keys
read once each on the default executor and system ticker: 208 to 368 keys across five trials kept
their original bucket and none were readable, and the deterministic fake-clock arm reaped them withEXPIRED at 3574s against the 3s they had asked for. A Scheduler does not shorten the window,
because getExpirationDelay reads bucket occupancy rather than deadlines and reports the next flush
3569s out. The delay ends at the sweep of the previous deadline's bucket, at or before that deadline,
so a dropped read never makes an entry outlive the schedule it already had; a lengthened deadline
self-heals the same way, its node sitting in an earlier bucket whose sweep pushes it forward.
Accepted: the node reference is gone once the offer is dropped, routing the reschedule onto the write
buffer would put most reads of such a configuration on the MPSC queue, and declining the CAS instead
would leave the entry readable past the duration the user asked for.
setExpiresAfter reschedules through the write buffer only for a shortening or for an extension
beyond the 1s tolerance. A smaller extension relies on the same self-healing, so oldest() andyoungest() order the entry by a deadline at most a second stale. A shortening keeps the write
buffer: a lost reschedule would leave the entry resident past its new deadline. The JCache adapter
calls setExpiresAfter on every access-expiry hit, and the UpdateTask made each hit pay what a
write pays. Alone, the skip took such a hit from ~2.2 us to ~195 ns on 8 threads. Beside the
adapter's own millisecond skip it saves ~25 ns more. Pinned byBoundedLocalCacheTest.expireTolerance_setExpiresAfter andExpirationTest.setExpiresAfter_reschedules.
The resolution is accepted for a cache's maximum-lifetime contract. It aligns with the 1s
expiration tolerance and pacer's minimum delay; a bounded cache can also reclaim by size.
An eager current-bucket sweep remains a possible optimization, not an approved change.
It costs O(bucket occupancy) per maintenance cycle: detach onto pending, deschedule, and
reschedule every not-yet-due node, instead of paying once per tick. Measure that cost before
removing delta <= 0; a nearest-bucket heap is another structure for stricter timer resolution.
expire detaches its bucket onto a field-backed, circular pending sentinel. Detachment
prevents a not-yet-due node from being rescheduled into the list being drained, especially on
the last wheel's single bucket. It does not prevent a nested RemovalTask from descheduling
an entry it already holds. A stack-local detached chain used to strand the remainder when
that operation cleared the carried successor's links. Preserve all three protections:
- The chain lives on a field (
pending.next), not a local, so a nestedunlinkof the head
repairs the walk's anchor through the ordinary path with no special case. The loop must re-readpending.getNextInVariableOrder()each iteration rather than carrynextacross the callback.
This is the legadvancingcannot cover: a nesteddeschedule/reschedulearrives viadrainWriteBuffer→RemovalTask/UpdateTaskand never callsadvance. Verified by keepingadvancingwhile restoring the stack-frame detach — the regression test fails identically. - Both lists stay circular. A linear or sentinel-less chain breaks
reschedule'sgetNextInVariableOrder() != nullscheduled-check (a tail with a nullnextreads as
unscheduled, so it gets linked into a bucket while still chained) and makesunlinka silent
no-op on the tail. The invariant is scheduled ⟺ non-null links, which is also why the in-flight
node's links are nulled before it is processed. A self-closing ring fixes the live-bucket splice
but not the stranding — verified by building it and watching the regression test still fail. advancedefers whileadvancing. A recursive advance would append its bucket to the same
list and splice the combination into its own bucket, and — the leg that matters more — it movesnanosforward underneath the in-progress caller, which then rewinds or skips buckets and leaves
entries permanently unexpired. It returns before touchingnanos, so the nested call is a true
no-op rather than skipping a time span. The predicate must be the explicit flag, not "ispendingnon-empty": the outer walk emptiespendingthe moment it unlinks its last node, so
the inferred version lets a nested advance through in exactly that window. That was a real
defect in the first cut of this fix, found by the fuzzer in 559 runs.
Pinned by BoundedLocalCacheTest.maintenance_recursive (a removal listener on a same-thread
executor invalidating a sibling and calling cleanUp()).
getExpirationDelay returns when a bucket is next flushed, not when an entry is due. A flush
either expires the entry or cascades it towards a finer resolution, so the wake-up is needed even
when nothing is due at it. expire covers ticks [previousTicks, previousTicks + delta], so a bucket
at offset b flushes at (b << SHIFT[i]) - (nanos & spanMask) and the current bucket — whose tick
has already passed — flushes at offset 1, one tick out, not a full SPANS[i]. Both places that
compute this must use that offset-by-one:
- The main scan clamps with
Math.max(1, j - start). It previously fell back to a wholeSPANS[i]
when the current bucket was occupied and returned before probingstart + 1, hiding an event one
bucket ahead that then fired up to a span late (68.7s / 1.22h / 1.63d on wheels 1–3; wheel 0 is
absorbed because both values are ≤Pacer.TOLERANCE). Reachable becausefindBucketchooses the
wheel by duration but the index by absolute time: a duration withinnanos & spanMaskofSPANS[i+1]wraps a full revolution onto the current bucket, so a far-future entry masks an
imminent one. Don't restore thedelay > 0 ? delay : SPANS[i]ternary — withbuckets >= 1the
result is always positive, so the guard is dead. peekAheadchecks the higher wheel's current bucket as well asticks + 1; both flush at the
same instant. This leg is benign on its own — only wrap-arounds land in a wheel ≥ 1 current bucket
(measured: over 300 random wheels no entry there is ever due before that wheel's next tick), so the
missed flush would only re-file an alias. It is honored anyway so the derivation "current bucket ⇒
only aliases" isn't load-bearing, and so the contract is assertable.
Partial-drain remainders are not affected — expireVariableEntries re-arms viaPROCESSING_TO_REQUIRED, so the backlog never depends on this delay. Pinned byTimerWheelTest.getExpirationDelay_occupiedCurrentBucket and by getExpirationDelay_fuzzy, which
asserts exact equality against an oracle scanning every bucket of every wheel. That fuzzy
assertion was previously vacuous — it guarded the assert with its own condition and compared an
absolute variableTime against a relative delay — which is why the defect survived the fuzzer.
The VarExpiration traversers list wheel 0's current bucket last. It holds the entries due
within the current tick and aliases a revolution away, so oldest() lists the former after entries
due up to a minute later, and youngest() lists them first. That is accepted under the views'
"roughly ordered" contract. Starting at the current bucket misplaces the aliases instead, and
splitting it into two visits must classify each timer once, since a concurrent read can move a
deadline between the visits and list the entry twice or not at all; the buffering that needs is
not worth it for a best-guess order. The traversers are also wheel-major and unsorted within a
bucket, so they can invert two entries by up to one span of the higher wheel.
Interner drainKeyReferences does not need a value-identity check. Unlike values, keys on
interned nodes never rebind. Cleared references now compare equal only to themselves. In the old
implementation, hash-colliding cleared keys could alias, but both polls still evicted both nodes;
only attribution swapped, unobservable with uniform Boolean.TRUE values and no listener.
Builder and bulk loading
LoadingCache.getAll is not atomic. Valid entries may commit before failure and are not
rolled back. Per-key null results omit that key; bulk maps must omit a key to mean no value.
An explicit null value fails the bulk load in both sync and async paths, matching Guava's failure,
though not its residue: Guava first stores every non-null entry, where the synchronous path keeps
only the entries before the first null and the asynchronous path keeps none.
Caffeine's builder mirrors Guava's CacheBuilder validation shape, asymmetries included.refreshAfterWrite(long, TimeUnit) checks the unit before state and requires duration > 0;expireAfterWrite / expireAfterAccess do not check the unit first and allow zero.
Consequently expireAfterWrite(-1, null) throws IllegalArgumentException rather than NPE.
Changing either order diverges from CacheBuilder and the adapter's compatibility suite.
Caffeine.from(CaffeineSpec) disables strict parsing, matching CacheBuilderSpec and
allowing programmatic overrides such as a weigher after maximumSize. The possibility of
accidentally disabling eviction is accepted.
Serialization
The SerializationProxy captures configuration only, and intentionally dropsexecutor, scheduler, and custom StatsCounter suppliers. Threads and
executors are runtime state, not serializable configuration; the deserialized
cache uses the defaults (common pool, disabled scheduler, default counter),
matching Guava's proxy behavior. Don't propose capturing them.
Serialization is same-version only, and that is not a gap to close (ruled
2026-08-15). The library never promises cross-version compatibility, and neither
does Guava; a promise of that kind has to be offered by the contract rather than
expected of it. So a finding of the shape "a stream written by an older release
deserializes into a broken or silently wrong cache" is closed on principle, not
on reachability or on whether the failure is loud. No serialVersionUID bump, noreadObject defaulting, no wire-format tolerance shim, no release note, no
javadoc qualifier. This covers SerializationProxy, CaffeineConfiguration, and
any serializable type added later.
Proxy field names and sentinel values are still wire format within a version.
Two changes broke cross-version streams: the loader → cacheLoader rename
(3.0.4) and the 0 → UNSET_INT sentinel change for the three duration fields
(3.2.4, the zero-duration fix). The reason to know this is not compatibility but
debugging a bug report: streams from ≤ 3.2.3 carry literal 0 for unset
durations, and a field absent from an old stream deserializes to the JVM
default (0/null), because field initializers do not run during
deserialization.
.Claude Docs Finding Taxonomy (`.claude/docs/finding-taxonomy.md`)
Finding Taxonomy
Standard classification for all audit and review findings. Referenced by the
auditor agent and all /audit-* and /review-change skills.
Severity
| Level | Definition | Example |
|---|---|---|
| critical | Violates correctness under any valid interleaving | Data race causing lost update |
| high | Violates contract under specific conditions | ConcurrentMap postcondition broken on retry path |
| medium | Correctness preserved but fragile | Works only because of current CHM implementation detail |
| low | Style or robustness concern | Missing null check on code path unreachable today |
State the affected configuration separately from the harm within it. Being non-default or in a
lower-priority module does not establish a severity ceiling; apply the module's stated use-case
scope and price the demonstrated impact.
Categories
| Category | Scope |
|---|---|
| memory-model | JMM access mode violations, missing happens-before edges |
| state-corruption | Weight divergence, lifecycle violations, deque corruption |
| specification | ConcurrentMap/Map/Cache/Policy API contract violations |
| resource-leak | Unclosed resources, unrecoverable state after Error |
| notification | Missing, reordered, or duplicated removal/eviction notifications |
| exception-safety | Incomplete cleanup on exception paths, phantom state |
| api-surprise | Public method returning nonsensical values for valid inputs |
| performance | O(n) on O(1) path, unnecessary allocation on hot path |
| redundancy | Removable work or state with a behavioral-equivalence proof and a concrete source simplification |
| liveness | Progress/termination failure without corruption: stranded drain status, lost wakeup, unbounded stall |
| external-contract | Misuse of a third-party or JDK API contract (dispose-on-error, merge-less collectors, live-view iteration) |
Severity must be priced on a realistic configuration
A severity is a claim about what a user can reach, so the reproduction has to run the
configuration a user runs. The test harness is built out of instruments that exist to make
behaviour deterministic, and each of them can manufacture an impact nobody can reach in
production:
- A frozen
FakeTicker. Several mechanisms self-heal simply because the clock moves.TimerWheel.advancesetsnanos = currentTimeNanosunconditionally, so withTicker.systemTicker()any operation reaching maintenance invalidates a live wheel iterator.
Freeze the ticker and that detector stops working, which turned a truncatedPolicysnapshot
into a reproducible non-terminating traversal holdingevictionLock. With a real ticker the
same scenario throws. The wedge was an artifact of the instrument. executor(Runnable::run)orCacheExecutor.DIRECT. Inline maintenance removes the
coalescing that hides per-operation cost and turns an asynchronous drain into a re-entrant one.
Use it to control scheduling; its timings do not price default-executor behavior.
Before assigning high or critical, re-run the witness with the defaults a user gets: the
system ticker and the common pool. If the impact depends on harness effects absent from the
claimed user configuration, identify those effects and do not attribute them to that configuration.
A mechanism that is real and an impact that is reachable are two separate claims, and the second
is the one severity encodes.
Confidence
| Level | Meaning |
|---|---|
| high | Concrete interleaving or input constructed; reproducible |
| medium | Plausible scenario but depends on timing or conditions not fully verified |
Omit low-confidence speculation entirely — it wastes triage time.
For redundancy, high confidence may rest on a complete source equivalence proof covering the
relevant exits, callers, and observations. Keep cleanup severity low and report test execution
and runtime effects separately. A measurable speedup or broken contract is not required; do not
invent one to justify the cleanup.
Classification (for triage)
| Label | Meaning |
|---|---|
| patch | Real issue, fixable in the current change |
| defer | Real issue but pre-existing, not introduced by this change |
| reject | Noise, false positive, or explained by design decisions |
| escalated | Cannot determine statically; needs dynamic testing |
.Claude Docs Hill Climber (`.claude/docs/hill-climber.md`)
The adaptive window climber
The deep reference for WindowClimber, the window-sizing loop BoundedLocalCache drives fromclimb: what it must do, why that is hard, the shipped design, and the graveyard of alternatives
with the reasons that killed them. Read this before touching determineAdjustment, the climber
constants, or the simulator's climbing package. The quick rules live in rules/design-decisions.md.
This is a design record, not a project log. Every entry answers one of two questions — what the
machine does and why, or what was tried and why it failed — and anything that only answered "what
happened on a given day" has been removed. Where a number appears it is load-bearing: it is the
price of a step, the bar a family is held to, or the measurement that settles an argument. Numbers
that can be re-derived by running the gate battery are not kept here.
wiki/adaptive-window.html is the companion design document, written for an external reader judging
the algorithm and its evidence. Internal QA logistics — test and fuzzer names, mutation baselines,
gate tooling — stay out of it and live here and in rules/testing.md.
Naming: the window climber is the whole controller (WindowClimber); its tiers are each named
for their steering signal — the reactive climber (≤4096, cross-sample ΔHR) and the density
climber (>4096, within-sample density ratio). The density tier in full is a goal-audited density
climber: density steers, probes rescue its blind corners, and the goal-metric layer (anchor, guard
rail, equilibrium audits) polices what density cannot judge. "density arm/tier" below always means
the steering component, not the whole machine.
The organising result of the original failure atlas — the steering rule rests wherecapacityShare = hitShare, not at the hit-rate optimum — is §2.1's identity, and the
marginal-steering thread it opened is settled in §5.
1. The problem
W-TinyLFU splits the cache into a recency-serving admission window (LRU) and a frequency-gated main
region (SLRU behind a TinyLFU filter). The best split is workload-dependent and non-stationary:
recency-heavy workloads want windows of 20–80%, frequency-heavy ones want ~1%, and real traces
switch regimes. The climber's job is to track the optimal split online using only signals the cache
can observe about itself, at zero cost to the request path.
Practical comparators include LRU, static windows and ARC on the same access stream, with MiniSim
testing what additional observation state can reveal. The objective is broad workload quality
within the cache's cost budget, not a universal policy optimum. Losses to these references remain
generic headroom; richer application metadata cannot explain them away.
A specialized policy may do better using context or structure, such as request category, turn and
prefix depth. OPT/Bélády instead knows future accesses. A gap to that oracle proves neither that
an online feature can recover the gap nor that learning such a feature must be expensive.
Inferring useful reuse structure from generic observations is a separate, testable question. It
requires bounded state and work, enough labels during cold start, an observation stream available
in the cache, and controls that distinguish persistence from scans and changing workloads.
The failed skew classifiers (§5's opening) do not rule out all inexpensive inference. Arbitrary
key hash values do not supply application semantics: do not treat them as addresses,
request categories or turns. Specialized contextual policies and generic adaptivity therefore
need separate evidence, while sharing the same discipline for information, action and cost (§6).
Constraints that shape everything:
- The counterfactual is invisible. The cache observes only what its current split earns.
What a bigger window would have caught requires ghost state (ARC's B1/B2), which Ben rejected:
heavy, implies untrue things, and does not pay off as caches grow. - Moving has real cost. A resize transfers nodes between regions (capped per maintenance cycle
byQUEUE_TRANSFER_THRESHOLDwith a multi-cycle carry-over) and displaces resident entries.
On corda_large the churn alone cost 10.5pp — every fixed window scored 33.33 while the moving
climber scored 22.8. - The goal metric is noisy at exactly the wrong times. A phasey workload swings the sample hit
rate ±10–20pp for reasons unrelated to the window, burying the window's marginal effect (often
under 1pp) — a cross-sample hit-rate comparison then chases the workload's mood. - There is no setpoint. Control-theoretic machinery (PID and friends) needs an error signal
toward a target; hit-rate climbing has no target, so integrators accumulate noise unboundedly
(the 2026-05 PID catastrophe: corda_lg −32 to −98pp, unfixable by any anti-windup variant).
2. The signals
- Cross-sample ΔHR (the reactive climber, ≤4096): bold-driver — keep direction while the hit
rate improves, reverse otherwise, decay the step, re-anneal to a 6.25% step when |ΔHR| ≥ 5pp
(RESTART_THRESHOLD). Goal-driven, so it can be confused but never persistently
wrong — the restart keeps it exploring. Weaknesses: churns on flat/phasey curves (corda), and
cannot cross wide plateaus deliberately (bold-driver flips on any sub-noise negative ΔHR, turning
a plateau crossing into a random walk). - Within-sample density ratio (the large tier's primary signal):
error = ln(windowDensity / mainDensity)withwindowDensity = sample.windowHits/windowMaximum
andmainDensity = (sample.hits − sample.windowHits)/(maximum − windowMaximum); stepsign(error) · min(0.30·max, |error|·0.03·max), window floored at 2% of max. Both densities are
measured in the same sample, so the workload's phase cancels in the ratio — this is what makes it
immune to the swings that defeat the reactive climber, and why it converges recency workloads to
their static-window ceiling (OLTP 1%→~21%, metaCDN, fiu_homes, corda's collapse fixed). - What density cannot see: it is resident-only and measures average (not marginal) value.
Two consequences: (a) bias — window hits are cheaper to earn, so frequency-optimal traces
give back ~0.5–2.5pp typically (worst observed 4.3pp: websearch2@4M holds a ≳10% window where
static-1% ≈ reactive wins, a fresh-holdout find; all still beat LRU by 11–61pp,
that cell by +24); (b) blindness —
a region earning ~nothing has an unmeasurable density, and the signal will happily hold or push
it further into nothing forever. - Signal hygiene (what feeds the counters): hits are recorded in
onAccess(read-buffer
drained, so real hits are ~5% lossy single-threaded — the drain was historically triggered only
by the offer after the fill; the buffer now reports full on the filling offer so the loss is
recovered), misses inAddTask. Only genuine usages may count: async load completions replace
quietly (no sketch increment, no hit counters — the UpdateTask only finalizes weight/expiry).
When loud, each async miss injected one synthetic, write-buffer-lossless, window-attributed hit
plus a doubled per-load admission frequency: the reactive tier wedged on phase shifts
(corda+loop @ 512 −12.7pp, deterministic) and the density tier read miss-heavy async traffic as
window-dense (w50 −38.6pp). Any new write-path task that callsonAccessmust decide
loud-vs-quiet explicitly — user-initiated writes are loud, internal bookkeeping is quiet.
2.1 The control-theory map
The density tier was derived from measurement, not from a textbook, but it lands on a named
architecture: a supervisory switching controller with hysteresis and state-dependent
average dwell time, wrapped around a certainty-equivalence inner loop, with an explicit
dual-control probing term — an event-triggered, non-dithering extremum-seeking scheme.
The vocabulary is worth carrying because three of the design's empirical facts fall out of it as
derivations, and because it names what a proposed change would be breaking.
| Mechanism | Formalism | Reference |
|---|---|---|
DensityClimber.steer on ln(d_w/d_m) |
certainty-equivalence inner loop; P-on-error into an integrating plant = I-on-the-balance-condition | Åström & Wittenmark |
| the log ratio itself | exponentiated-gradient step on the capacity simplex (scale-free by construction) | Kivinen & Warmuth 1997 |
| its rest point vs. the optimum | average- vs. marginal-value equalization | Stone, Turek & Wolf 1992 |
| anchor + guard rail | hysteresis switching; VETO_MARGIN_MIN is the constant h |
Morse, Mayne & Goodwin 1992; Hespanha, Liberzon & Morse 2003 |
VETO_STREAK, AUDIT_COMMITMENT, Ladder.commitmentDepth, Anchor.freshLeft |
dwell time | Morse 1996/1997; Liberzon 2003 |
| the doubling ladders | average dwell time with chatter bound N₀ |
Hespanha & Morse 1999 |
| probes and audits | dual control — steering has no dual effect, the probe supplies it | Feldbaum 1960–61; Bar-Shalom & Tse 1974 |
| the probe verdict (retain unless refuted) | unfalsified control — no plant model, data from the active controller only | Safonov & Tsao 1997 |
| starvation bar | loss-of-persistent-excitation detector | Boyd & Sastry 1986 |
| confirm / veto streaks | sequential change detection | Page 1954; Basseville & Nikiforov 1993 |
| terrain B, the moat, the rung-scaled stride | non-local stability of extremum seeking (escape needs large-amplitude excitation) | Tan, Nešić & Mareels 2006 |
Three consequences worth stating outright, because each replaces a fact currently carried as
folklore:
- "There is no setpoint" is true of the goal metric and false of the loop. The command is a
change in window size and the cache accumulates it, so the plant is a pure integrator and
proportional-on-erroris integral action with setpointerror = 0. The 2026-05 PID
catastrophe was not "integrators explode here" — it integratedHR − target, an error whose
zero was invented, through a plant that already integrates. The density arm integrates an
error whose zero is a physical balance condition. The design decision was to move the
integrator onto a signal that has a true zero, which is why no anti-windup variant could
rescue the old one. - The rest point has a name and the recency bias has a derivation.
error = ln((H_w/C_w)/(H_m/C_m)) = logit(hitShare) − logit(capacityShare), soerror = 0
⟺capacityShare = hitShare: average-value (proportional-share) equalization. The
optimum equalizes marginals,H'_w = H'_m. For hit curves concave through the origin,
average ≥ marginal, and the region with the larger average−marginal gap is over-allocated;
the window is an LRU serving tight reuse, so it saturates fastest and has the larger gap.
That derives the documented give-back ("frequency-optimal traces give back ~0.5–2.5pp") and
its direction. It also says what the goal-metric layer is: not a patchwork, but the
correction term for a bias with a closed form. Terrain shape B is simply non-concavity, where
any first-order matching rule rests self-consistently — F-4 is the definition, not a surprise. - The ladders imply a duty cycle, and the duty study measured the real one.
Worst-case arithmetic (a 17-sample walk against each rung) overstates the machine 2–4×: over
6193 density samples on the 49-cell battery the mean armed excursion is 4.2 samples
(starvation) and 7.7 (audit), and the occupancy split issteer42.2%,park25.9%, audit
excursion 19.0%, starvation excursion 5.9%, blind-cornerhold6.1%,undo1.0% — the
machine commands nothing (32.0%) more often than it explores. (R4-F1's "6% → 43%" shieldtrap
occupancy was the round-3 clock; at the shipped tree that cell reads 13.4% park + 13.4%
walk.) A crash-prone workload sustains
audit duty indefinitely (~11% at cycle ≈7.7+64:crashnoise_a12,whisper_mod_a12,h4c1_attack), because the wait reaches 512 only through completed deepest-rung failures
while a crash deliberately keeps the ladder cadence — R4-F1's shape, structural. Two
refutations from the same study: the constants are not derivable from one declared duty
number (a declared audit band recovers two of the four), and no duty or dwell bound can
see the deferral defects — H4-C1 and adv4-F2 violate the schedule's provenance, not its
magnitude (maxOverduemeasures 0 through the whole 130-sample H4-C1 pin). What the duty
budget does generate is the C2 progress clause (§6).
Two caveats so the map is not over-claimed. Morse's hysteresis lemma bounds the switch count
only for monotone or exponentially discounted monitoring signals; Rates.smoothed is a
symmetric EMA whose Anchor.rate re-syncs on-anchor, so the finiteness bound does not transfer
as written (Hespanha–Liberzon–Morse's discounted monitoring signals are the standard repair, and
would give a stated bound on anchor churn). And "scale-independent hysteresis" in that paper is
the multiplicative form μ_p(1+h) < μ_q; the rail is additive with a noise-adaptive margin —
related, not the same.
The map also predicts the rail/audit split the F1 study found by measurement: in dual control the
cautious branch is conservative in the loss metric while the probing branch is optimized
for information, so the two want opposite pricings and must not share one margin. That has now
found three instances, the last of them inside a single walk: the rail's margin against the
audit's confirming streak (F1), the starvation probe's bar against the audit's (adv3), and the
audit walk's own crash abort against its reversal (§4). The third is the sharpest
reading of the principle, because the two exits are five lines apart in the same walk and shared
one threshold: the crash abort is the cautious branch, priced on the loss it is measuring, while
the reversal is the probing branch, which must not turn around on evidence it cannot distinguish
from noise. Where a proposed change makes one bar serve two questions, this is the shape to check
for. One open
direction comes from the same lens: a detrended confirm reference (bothWalk.beatBase and Anchor.freshLeft exist because the reference is an un-detrended level, and
"a trend clears any raw streak" is what detrending removes by construction).
The map's other two suggestions were built and refuted by the derived-guard study
(§5). Freezing the walk's deviation reference at arm is wrong because that feedback is
load-bearing, and a within-sample confidence interval is both too small to bind and answers the
wrong question for the position jam. Read those graveyard entries before proposing either again:
a formalism that names a mechanism correctly still does not license changing it.
3. The adversarial cases (the ones that must always be re-run)
The generators, run commands, and per-family verdicts for this section live in the committed/climber-gate skill (.claude/skills/climber-gate/); the traces are deterministic and
regenerate on demand, so only the generators are preserved. New families are found by/audit-regret, which searches a compositional workload generator for cells where the machine
does not close the gap to its static ceiling and classifies each failure (wrong equilibrium,
slow convergence, masked signal, insufficient exploration, oscillation, memory, irreversible
damage, aliasing, premature commitment, tier discontinuity, structural) against the layer that
owns it; a confirmed family lands here and in the gate table.
corda + 5×loop + corda phase-shift stress (real traces, bundled): run at
512/513/1024/2048/4096/4097/8192; no cliffs at either tier boundary; near static ceiling.mixture family (
gen.py mixture, seed 7): steady Zipf
hot-set (defends main, feeds mainDensity) + twice-accessed items at reuse distance
D ∈ (2%, 25%]·max. No phase structure. Pure density pins at the floor for 100% of adaptations
and lands ~28pp below LRU — at 8192 and any scale where D falls in the band (the trap is
scale-relative because the floor is 2%·max). This is a plausible production shape (popular
content + session-scoped re-reads) and the reason a tier threshold alone cannot make density safe.phases family: alternating loop-over-0.9·max and pair-reuse-at-D phases; d050 (D=50%·max)
traps pure density at 8192 AND 32768. It is bimodal with a 14pp run-to-run range (the
admission tiebreak picks the basin), so §6's ±0.1–0.8pp noise model does NOT hold and
single-run numbers here are uninterpretable; the trace-start alignment offset alone is worth
up to 12.5pp, frozen for the run because the sample grid never dithers (no aliasing comb at
commensurate ratios — 38 ratios × 12 reps, mean +0.62pp mixed sign). What is real is the lag
limit: phases shorter than ~2 sample periods (~8·max requests) are untrackable. Current
classification: d050 is one of the three genuine steady-state pins, −8 against
LRU converged, its grid-locked cadence constructed-only.deadphase: hot-set + pure one-shot scan bursts. Proves exploration during dead samples is
~free (admission shields main) — all variants sit at the ceiling. But the safety is a knife
edge on the ε symmetry, not on the dead sample (failure atlas): a fully dead sample
givesln(ε/ε) = 0, a no-op; break the symmetry with as few as six window hits and the same
sample yieldserr = +17.4and the full 30%-of-max step. A rider of 380 requests in 1.97M
(0.019%) riding a victim's own scan phases costs the victim 8.3pp and drags the window from
0.03 to 0.48; at 0.4% it pins the ceiling for −9.6pp. Delivered througharmProbe's
refractory fall-through, which suppresses the probe but not the density arm. Reactive tier at
4096 on the same trace: −0.44pp.widepin: whole-working-set alternation (pairs at 0.6·max ↔ loop over 0.85·max). A fixed
window wins by never moving; every online climber pays here. Re-classified: the
whole-trace deficit is mostly warmup (it converges to a 77.9% window against an 80%
optimum, final third −3.7 vs LRU), and the row is seeded-only — its unseeded distribution
is a 50/50 basin coin whose old bar sat at its own mean. Bar: no per-seed drift, seeds 1–8.Thin-signal floors (w50@123038, S1/S2/S3, DS1): frequency-ish traces whose floor window
earns wh/n in the same band as a trapped window (w50: 131–437 hits per 492k sample vs trap
strays ≤57 per 32k). No resident-only threshold separates them — this is measured, not
conjectured, and it is why probes+adjudication exist instead of a smarter starvation predicate.lowmix (
gen_attacks.py, seeds 7/11/13): low-HR (2–7%) bistable shape whose probation
concentrates the reuse band while the good state is an LRU-ward diluted window — the marginal
verdict's named trade (the frozen baseline false-vetoes the escape that the rejected
diluted-average form confirms by seed-luck). Sentinel: no drift below the recorded values.demoflood (
demoflood.py): protected-saturating hot core + walk-reachable band — the
constructive proof that live-probation adjudication is an absorbing false-veto and the
pre-walk freeze is earned. Gate row: frozen-v5a must stay far above the live-variant's pin.whisper + window-irrelevant modulation (
whisper_mod.py) and mixmod
(gen_adv.py mixmod): the F1 dose instruments — a hit-rate modulation that provably does not
change the optimal window (pin-sweep verified) must not turn the goal-metric layer off. The
deviation-priced confirm failed this dose-responsively; owned by the raw-sample streak
confirm and the fresh-park shield, with the deep a0.12 dose (whose scatter crosses the
audit's crash bar) closed by the crash-streak time persistence — the sentinels now sit
within half a point of LRU.mixmodreads −2.4 against the reactive arm and is
constructed-only.esc_jamandtenant_s10ride along as
jam-family and co-tenant-family sentinels for the same layer.shieldtrap / climbtrend / loopcliff (
gen_adv.py, round-2 instruments promoted to gate
rows by round 4): regime-change-after-confirm, trend-driven misconfirm (plus the flat+wavesaw_p40variant), and the no-cliff invariant at the structural misconfirm landing.shieldtrapandsaw_p40are the audit-amplification sentinels (R4-F1): the round-3
clock repairs multiply audit reachability — worth +10.4 on the attack rows, giving back
−1.0..−2.1 on these already-below-LRU synthetics (audit share of the run 6% → 43% on
shieldtrap, which also turns bimodal) while twelve real density-tier cells move ±0.07.
The rows exist so the next audit-schedule change is measured against the cost side too.
Part of that give-back was never the schedule's: the audit-bar split recovers
+0.79/+1.75/+1.17 (N=8: 78.06/77.19/77.44 → 78.84/78.94/78.62, re-based in the gate table) withsaw_p40at 0.00, becauseAUDIT_BAR_FRACTIONwas pricing the walk's reversal at a median
0.28σ here and killing walks on samples the machine itself calls noise. Attribute the remainder
to reachability, not all of it.posjam (
gen_jam.py, round 3's position-jam instruments): the whisper base with the
whisper dose modulated at the sample cadence instead of constant — dose-matched and
provably window-irrelevant (LRU and every static window identical to the flat control). The
modulation makes the density arm command super-band steps, which starved the hard-reset
audit clock forever (66.9 → 56.1 = the audit-free pin, −8.9 below LRU; scale-invariant at
16384). Owned by the decaying clock and the audit-owned schedule (thej50±50%-jitter andd25period-2.5 rows recover to 66.4–66.5); the sample-aligned every-sample variant (d0)
still jams — it needs ±¼-sample grid alignment (the D4 exposure class), is a deterministic
9.3pp regression against the reactive arm, and is constructed-only, priced and parked
(the stillness-measure study stays deliberately unspent). A slow
square duty re-creates the starved clock below every-sample motion (the Sol
round'sentry_duty, a dose note on theposjam_d0gate row); the loss there is the
rider's class-3 mask, not the jam.
The optionalaudit-regret/specs/bandrider.jsoncontrol uses a finite-distance rider
(pairs at 0.15·max) with a sine-modulated share, a steady far band at 0.67·max, and a
Zipf core. At maximum 8192, workload seed 1093 and 256 continuous samples, admission
seeds 1–8 give hybrid 51.44 (51.36–51.52), bit-identical tonoaudit, against
62.01 for reactive and a 64.94 best tested static reference. All eight default-start
histories have no audits after one early starvation confirmation. The later 128-sample
suffix still loses 12.92pp to the same fixed 45% reference, so the deficit is not only
startup. Holding probation at its live 1622-entry allocation gives 64.78–64.90 at
43/50/60% windows; actual adaptive starts at 43% and 60% give 65.28 and 65.37. These are
different initial histories, not frozen decision counterfactuals. The observed small-window
orbit is path-dependent. Eleven one-factor neighbors at 128 continuous samples retain 13.4–13.9pp gaps at one
admission seed (amplitude, period, phase, distance, scale and trace seed); a lower modulation dose
escapes later. Its amplitude ladder mixes delayed escape, unsuccessful audits and audit
absence, so a hit-rate threshold does not identify a mechanism boundary. This is another
class-3/class-5 dose besideentry_duty, not a new failure class or mandatory gate bar.
No individual missed audit's benefit was measured.
Theaudit-regret/specs/stillness_starve.jsondose is the class at its extreme: a trickle rider
and a two-reference band, square-modulated together, keep 63–90% of samples moving, so not one
audit arms in 128 samples, the calibration audit included. Seeded 1–8 at 8192 it reads
51.16 ± 0.13, identical tonoaudit, against reactive's 56.08 and a 60.0 static peak, at every
period from 2.3 to 9.0 samples. The shrink (stillness_starve_min.json, the band dropped) keeps
the mechanism, and holding the rider steady arms four audits. Another dose, not a new family.crashnoise / mixnoise (
crashnoise.py,mixnoise.py, adv3 rows): the whisper and
mixture bases under a mean-centred, RMS-normalised rate modulation whose amplitude is
the dose — the walk's exit-bar instruments. The two interior exits test different statistics
(crash abort = level vs the rate frozen at arm; reversal = first difference vs the previous
sample — §4), so the dose ladder is a threshold, not a gradient: nothing moves under the
bar, the cell falls off where the scatter crosses it.mn8_sine_a10was the defect on the
starvation-probe path — the wave crash-cycled every up-probe one stride off the floor,
severing the mixture trap's only escape — healed by the probe-side pricing (51.6 → 60.7,
+5.6 over LRU).cn_sine_a12, the audit-side sentinel, is bimodal (its "deterministic"
record was an N=2 artifact) and healed most of the way by the crash-streak time persistence
(−2.8 → −0.8 vs LRU); the residual is the crash abort's robustness price.whisper (
whisper.py): a mixture base plus a ~0.2% immediately-re-read trickle sized to
keep the window's sample hits JUST above the starvation bar, so no blind corner is ever
declared and no probe arms — the sharpest F4 instrument (pinned −9pp below LRU at 8192, −12
at 16384, knife-edged exactly onMIN_SIGNAL_SHIFT; the quarter-dose variant drops below
the bar and the marginal verdict escapes where the average form stays pinned). Owned by the
equilibrium audit: 66.8 against LRU 64.6 on every seed, identical under both verdict forms
above the bar.shallowmoat (spec
audit-regret/specs/shallowmoat.json):
a zipf over 1.5·max that fills main and gives the window only stray hits, a two-reference band
6,200 requests apart caught only past a ~58% window, and a one-shot scan that keeps probation
thin. The static curve is a valley 2pp deep and 57% of the cache wide with a 19pp prize behind
it, so no walk crashes and no first-round walk reaches it. Seeded 1–8: 27.4–28.5 against a
42.0 ceiling and LRU 39.4, and the same product planted at 70% holds 42.0. Two basins, one
outcome: the starvation up-probe adjudicates at 4×bar hits three or four strides out, confirms
against the thin frozen probation,Ladder.rewardresets the rung, and the density arm reverses
the window in the same sample, so the ladder alternates 1↔2 and no commitment depth is ever
reached (absorbing at 127 decisions); or the calibration audit confirms on the trace's warmup
trend at 32%, one stride short of the cliff, and parks. Reach (class 4) through a successful
verdict erasing the ledger (class 6). Splitting the band over a spread of reuse distances keeps
the gap (26.8 / 29.0 at N=8) and adds the average law's rest-point give-back from above. The
reactive arm beats the machine on 6 of 8 seeds (+2.0 mean).
Reach was repaired the same day (§5): a confirm the density arm reverses now
deepens the ladder instead of resetting it, and the wedge seeds cross the cliff at s17 (30.7 /
30.0 / 30.6 against 28.5 / 28.4 / 28.5). The row still reads far below the ceiling because the
found position is not kept: after a walk from the floor, density's rest point on the far side is
at or below the cliff (main holds the protected core, 3.7 hits per entry against the window's
1.2), the guard rail cannot catch the fall (the fall inflates the deviation its margin is priced
from), and the deep walk's confirm at the top is itself reversed, so nothing parked. Retention
landed: a reversed confirm at the deepest commitment that the goal metric
confirms parks as an audit's does, and the park survives its own audits' crash-scale moves; the
wedge seeds read 36.5 / 36.4 / 36.3 (repeat 2: 37.1) against the 42.0 ceiling, the residual being
the top corner's periodic down-audit crashing at the cliff. Two facts from/audit-regretround 2
: the basin is decided by the first sample's window hits against the bar (30–31 hits arm a
probe at s1 against a fat frozen probation, which fails, basin B; 32–34 leave the floor sighted until
s3, when a probe against a thin one confirms, the wedge), so a regenerated trace reshuffles which seeds
cross; and both repairs live on the starvation ladder, so a rider that keeps the floor sighted removes
them:veilmoat(the same trace plus a k=8 burst re-read 40 requests apart at 0.06% of traffic, worth
0.3pp) arms no starvation probe on any seed, and every seed then takes the audit path (26.84 ± 0.34 at
64 samples on 8 of 8 against a paired unmasked control of 4 of 8 crossing; the rung-32 up-audit crosses
at ~s88, 32.02 at 128 samples), and after the recorded top-corner loss the guard rail's veto and
density's slide form a ~17-sample limit cycle with no re-escape (32.45 ± 0.07 at 256 samples, where the
unmasked geometry re-escapes through the deep starvation walk to 34–40). The mask holds from ~3e-4 of
traffic (below that the ladder is the parent's own lottery); a dose note, not a family. A park's first
audit following its walk turns the basin-B seeds' second audit up from the 32% park
into the cliff crossing at s52 (27.4 → 30.1 on the 64-sample row, 32.2 → 34.0 at repeat 2; the wedge
seeds identical) and does the same forveilmoat(26.84 → 30.54 on 8 of 8, 32.02 → 33.93 at 128
samples, 32.45 → 33.11 at 256); the retention residual after each escape is unchanged.absolve (specs
audit-regret/specs/absolve.jsonandabsolve_p8.json): a lure population (pairs 1,300 requests apart, 10% of traffic) pulsed on and off
with a period of 16 samples inside a wide flat valley, a far band (pairs 6,000 apart, 45%) caught only
past ~54% of the cache, and a zipf core in main. Static: 18.8 to 10%, a +5pp shelf from the lure's knee
to 50%, 52.5 at 70%; LRU 51.5. Seeded 1–8: 22.59 ± 0.18 at 64 samples, 24.77 ± 0.11 at 128, an escape at
s144–159 (36.4 at 256 on seeds 1–4), reactive 25.5 and noaudit 22.9 trapped as well, and the same product
planted at 70% holds (51.07 ± 0.21). Two mechanisms by trace length. Through 64 samples it is the audit
tier: the calibration audit's level-test confirm at 32% on a phase step (the lure switching off raises
main's hits by 5pp against a stale reference), a 32-sample park, the down-first alternation and its crash
when the lure returns mid-walk, the undo's arrival discarding the anchor:shallowmoat's basin B at
another dose, on every seed. From s60 a cycle paced by the lure: the off phase blanks the window and the
corner arms a ×1 walk, the on phase confirms it at 2,200–3,400 on the lure's own hits (the verdict cannot
attribute the watched region's hits, and the lure is dense per slot and worth nothing above its 8% knee,flood_j100's shape pulsed), density rests on the lure at ~37% then slams the window home when it goes
off, and the ladder gains one rung per period because it gets one walk per period, with the kept
confirms resetting it to 1 (Ladder.reward); the escape needs a walk longer than any first-round one
(six ×1 strides from an arm caught mid-slam at ~1,650, or the rung-32 ×2 walk) and comes ~140 samples in.
The average law also abandons a partial catch of the band at 4,300–4,400 (window density 1.30 against
main's 1.59). At a period of 8 (absolve_p8, 256 samples) every confirm is kept, the rung reads 1 or 2 on
223 of 255 samples and never reaches 32, the audit clock never fires (the position never stills 16
samples), and the cell is absorbing: 27.92 ± 0.19 against a 57.4 ceiling and LRU 55.6. Robust to the
grid (period 12: 26.35 ± 0.16 at 128 samples; a phase offset: 28.52 ± 0.86; period 20 a 2-of-8 escape
lottery), to the band distance and to 16384 (18 of 18 neighborhood cells at 32–34pp at 64 samples), and
a knife edge on the lure's share at ~1.6% of traffic, where the calibration audit's direction flips from
up-from-the-floor to down-from-an-interior-position. Reach (class 4) through the verdict's attribution
(class 3), density's chase (class 5) and the ladder reset (class 6); the period-8 form is class 6
outright, thedeferrewardshape (§5) made deterministic. Repaired: the starvation ladder prices a confirm at or
short of the farthest window its walks have confirmed as a completed experiment, so the period-8 form
escalates 1 → 64 over six periods and the rung-64 ×4 walk crosses the valley in two strides (27.95 →
46.29 at 256 samples, above the reactive law's 45.28); and a park's first audit follows the walk that
confirmed it while the claim stands, so the period-16 form's second audit walks up from the 32% park
into the band instead of down into the lure's knee (24.77 → 41.46 at 128 samples; 36.45 → 46.89 at 256;
period 12 and the phase offset 26.4 / 28.5 → 44.0 / 43.1). Period 20 stays a 2-of-8 lottery. What
remains after each escape is the recorded top-corner residual (the down-audit crashing at the cliff and
the undo's arrival discarding the anchor).ghostclaim (spec
audit-regret/specs/ghostclaim.json): two
phases, a zipf core with a band 2,000 requests apart and a scan for 28 samples, where density rests
at ~43%, then a sleeper population 6,400 apart at half the traffic, caught only past a ~64% window.
Static: 19.9 at the start, 54.1 at 70%; LRU 51.8. The stale-claim family's away-anchor case: the
calibration audit's down-walk re-syncs the anchor's claim to phase 1's rate as it passes 20%, density
holds the window at 43%, and the shift lands with the window still and off the anchor, so the
stand-down keeps the claim (the carve-out). Seeded 1–8: 41.88 ± 0.17 at 64 samples (the s37
up-audit sits at the top for ten samples at +30pp and fails at budget against the claim), 31.24 ± 2.10
at 128 (the same claim then vetoes the window to the phase-1 anchor and its hold, the down alternation
and the deepest-rung wait pin it), reactive 37.96 (bimodal), noaudit 30.30. Knife edges by
construction: the stand-down's band (a phase-1 band share of 0.16 lands the shift on the anchor and the
claim is discarded) and the prize's rate against the claim (a sleeper referenced three times clears it).
Class 6 with class 4 as the pin. Repaired: the audit's walk is measured against the smoothed rate it leaves rather than the anchor's
claim (31.24 → 48.45 at 128 samples, above the reactive law; the witness 41.88 → 46.86;cp_w100+2.0
on the corpus). The discard shape died on the moat rows (§5). What remains is the shift landing on the
audit's arm sample or during its walk (ghostclaim_p35..p40), where the walk crashes on a
contaminated base and the stale veto's hold pins the machine, and an audit arming inside the smoothing
horizon after a shift, which measures against a blend of the regimes.crestpast (spec
audit-regret/specs/crestpast.json): a
uniform bulk over 1.2·max at 72% of traffic (main's population, its hits linear in main's capacity)
and a two-reference band 1,100 requests apart at 25%, caught by an 8% window. Static: 45.8 at 1%,
44.7 at 5%, 62.5 at 8%, 64.4 at 10%, then a decline of ~0.9pp per 6.25% stride to 54.3 at 80%; LRU
52.3. A crest 17pp high, one stride from the floor, with a cliff on its near side and a slope on its
far side. The calibration audit crosses the crest on its first stride and confirms on its fifth
(AUDIT_CONFIRM_STREAKcompletes at stride 4,AUDIT_COMMITMENTholds the verdict to stride 5),
so it parks at 33% earning 3.2pp less than the crest it walked over; the park's audit walks back
down, has the crest under foot at its fourth stride and its fifth on the cliff side, where the level
test crash-aborts beforeauditEndingcan be reached; the undo's arrival discards the anchor (the
C2 shape) and density's average law, which reads the caught band as a dense window against a thin
main, drifts the window up and away from the crest until the next audit fails at the corner and
the rail vetoes back. Seeded 1–8: 57.93 ± 0.04 at 132 samples against 64.9 at 10%, reactive 46.5
and noaudit 46.2 both pinned at the floor (the hit-rate law steps into the 1–7% decline and never
crosses; density steers the sighted window down), and the same product planted at 10% leaves the
crest (62.4). The verdict's position is the mechanism: the confirm lands at stride max(k + 3, 5) for
a crest crossed at stride k, from either side, so the two endpoints the machine visits (the floor
and the floor's fifth stride) can neither of them confirm the crest, and what the machine holds
between audits is the average law's rest point above it. Planted at 40% the down-audit's fifth
stride lands on the crest and confirms there, and the row still reads 59.7 (gap 5.0): a confirmed
crest is lost after one audit cycle (the corner audit's fail, the veto, the second park's crash,
density's drift), so the retention half is the larger part of the steady loss and the verdict's
position decides where the first park lands. Each stride's sample is an arrival transient, so on
most cells the walk's best sample is one to two strides past the static crest, which bounds what a
verdict at the walk's best could recover.
The far side's slope prices it: bisecting the bulk's size finds the knee at 0.68·max, main's capacity
at the park, below which the bulk still fits from the parked position and the overshoot lands on a
shelf (demoflood's recorded overshoot, which the C2 discard there accidentally corrects because
density's rest point sits at that crest). Robust across the band's distance (800–2,200 requests),
the bulk's size (1.0–1.5) and 16384 (ten cells at 4.4–8.0). Class 9 (the audit verdict,Walk.isConfirmed) with class 1 holding the loss and the C2 discard handing it over. With the crest
beyond the fifth stride (the band 4,000–4,400 requests apart), the same verdict parks the window in
the valley on the cold reference instead,regimeramp's recorded misconfirm at ten times its
sentinel's dose (s_fatbase: 29.6 against 44.3 at 45%, LRU 37.2, at 72 samples).hazefloor (
/audit-regretround 3's note, rowed by round 4 at 320 samples; specaudit-regret/specs/hazefloor.json): a uniform haze over 2·max, a zipf core, a band 7,200 requests
apart caught only past ~60% and a scan; static flat 33.7 → 32.5 to 50% then 52.3 at 65% (a 60/65%
re-sweep), LRU 49.2. The calibration audit crosses and parks at the top by s24, and the rest of the
run is the top-corner residual by itself with a ~105-sample period: the corner's audit is forced down,
crashes at the cliff, the undo's arrival discards the anchor, density slides off the cliff, the
floor's ×1 walks fail against the haze, and the rung-16 audit re-crosses. Seeded 1–8 at 320
samples: 41.09 ± 1.80 (38.3–42.0), 8pp below LRU, against the reactive law's 36.5 and noaudit's
37.5, so the layer earns its keep and loses most of it every cycle. A sentinel with per-seed bars,
not a pass bar; the row exists so a change to the corner audit, the C2 discard or density's slide is
measured against it.parkveil (specs
audit-regret/specs/parkveil.jsonat
240 samples andparkveil_min.jsonat 60): a zipf core (0.5·max, α 1.05) and a two-reference
band (pairs at 0.45·max) whose shares alternate in a square wave at period 13 samples, plus a
10% one-shot scan. The fixed mid window wins by construction (ceiling 66.82 at 50%, an interior
plateau wholly above LRU 66.04; the product planted at 50% holds 67.03), and every adaptive arm
sits below LRU: seeded 1–8 the machine reads 53.13 ± 4.09 (a 6-vs-2 basin split),noaudit
55.73 ± 0.55, the reactive law 59.91 ± 0.92. Density's rest point alternates with the mix (the
ratio's within-sample phase immunity holds for rate swings, not mix swings), each rec phase
blanks the window into a blind corner, walks crash on the phase boundaries or confirm on the
flips as wedges (the ladder escalates 2 → 64 as designed), the rung-64 walk parks at the top
corner as an audit-grade confirm earning within ~2pp of the plateau, and the park survives
exactly one fresh-park shield: flips inside the shield are absorbed, the park's own audit crash
returns to it (the cover), and the first post-shield flip firesisWorkloadShift,
whose stand-down discards the claim at the anchor and re-seeds the goal metric, so the position
is re-derived through the full ladder every cycle. The guard rail never fires on any seed: the
phase swing holds the deviation at 0.21–0.34 and the veto margin at 62–101pp. The cycle is
arm-independent (noauditparks at s134 through the ladder alone and re-parks more reliably;
the audit layer's margin here is delay from a calibration flip-misconfirm's reward reset, plus
variance), so the loss lives in the shared stand-down and shield machinery. The last-third gap
is 6.0 of the whole-trace 13.7 (half the headline is one-time ascent); a ~2pp per-cycle
re-derivation cost and a 2-of-8 lost-seed tail persist. Robust across the period axis (gaps
27/27/7/27/12/17/12/13 at periods 2–32, a resonance structure with one narrow escape at 6),
amplitude, band distance (deepens to 22.1 at d=0.6), 16384 (21.2), phase offsets (6.7–20.2,
none clean) and trace regeneration (~2pp milder). Class 5 (the chase; controller) with class 6
(the discard; recovery layer); the census C3 observation's first cost-bearing cell, and the
mirror offlashpark(there the shield holds a wrong park through a regime end; here the
shield's expiry ends a right one).latebloom (specs
audit-regret/specs/latebloom.jsonat
262 samples andlatebloom_min.jsonat 187): a zipf core (0.15·max, α 1.0) at 60% of traffic
and a scan for 112 samples, flat within the audit's level test at every window, then a
two-reference band 7,000 requests apart at a third of traffic for 150 samples, caught only from
a 50% window. Static: 52.4 from 1% to 40%, 66.4 at 50%, 66.2 at 70%, 65.9 at 80%; LRU 64.5.
The prelude spends the audit schedule: the calibration audit parks the floor on the cold-start
lag, the park's two audits walk their full budgets to the 80% cap on flat terrain and end
failed rather than crashed, and the second failure at rung 64 doubles the clock to 128
(AuditClock.reschedule). When the band arrives the floor's rate drops 15pp and the stand-down
fires, but a stand-down leaves the audit rung and the clock's wait untouched, so nothing is
rescheduled; at the base's alignment the arrival lands inside the undo's retreat, whereisParkTestcovers it, and the held floor survives the change outright. Either way the
machine sits at the floor against its own law (rest_err +1.3) until the 128-sample wait runs
out and the next audit catches the band in two strides. Seeded 1–8 the base reads 54.90 ± 0.04
against the 66.4 ceiling (gap 11.5, 18% of the prize, 123 samples parked),noaudit53.39
pinned at density's ~19% rest point (the core's head keeps the window sighted at ~4,000 hits a
sample and worthless at the margin, and the band is a +19pp step between 27% and 51% that a
marginal law cannot see) andreactive53.44; planted at 50% it reads 57.85, since density
slides off the flat prelude before the prize exists. The loss is the latency and nothing else
(gap ≈ (confirm − arrival)/n × 23.9pp fits every cell within 0.5pp), and the phase-1 ladder is
the family's shape: an arrival during an audit's cap-sit is confirmed at once (56/104 samples:
0.9/0.8), one on a walk's early stride crashes it and is found at the next wait (48/96:
3.5/4.4), one just after a budget failure releases the park by the stand-down, lets density
rest at ~19%, and the next audit alternates down from that interior position and fails into
the 128 wait (64/80: 16.7/15.4, found at ~s247 when the horizon allows), and one during or
after the retreat is found ~125 samples late (112–132: 9.8–11.5). Over arrival times the mean
is 7.2, the median 6.2, the worst 17.1, and about a tenth are found at once; the base is the
76th percentile. Identical at 16384 and flat in the band's distance down to 3,500 requests,
where the held floor still catches nothing although density's rest would; the minimal witness
(187 samples) is the never-found form, 55.78 ± 0.02 againstnoaudit55.79. Class 4 (the
exploration cadence,auditWait, andchooseDirection's side choice from an interior rest
point) on a class-3 terrain; the retreat cover's hold is incidental above the family's
boundary. Nearest recorded: H4-C1 andmetronome, which reach wait 128 through crashes on
trap terrain; this reaches it through the designed path and prices a detected regime change
that reschedules nothing. The latency face of that residual, whose
re-arm was priced dead (§5's re-arm entry: the stand-down alignments' gaps fall to
5–6 and the price lands on the F1 sentinels).mainsat (
/audit-regretround 7; specaudit-regret/specs/mainsat.json, 72 samples): the
main-side masked signal, the sighted lane's directed slot for seven rounds. A zipf core over
0.45·max at 30% of traffic (recurrence ~12k requests, longer than the window's residency at
every window, so main holds it and the window never takes it), a band 5,200 requests apart at
18% caught from ~50%, a 52% scan that keeps the window's residency short enough for the core to
stay main-resident even at 80%, and a gap-1 trickle at 0.3% that keeps the window sighted.
Static, dense-swept: 20.3 flat to 40%, 27.2 at 50%, 32.0 at 52%, 35.8 at 58%, 35.4 at 62%,
34.6 at 70%, 33.5 at 80%; LRU 30.5. Main's 7,000 hits a sample are real and worth nothing at
the margin (main holds 8,028 entries for a 3,686-key set), sosteeringErrorcommands a
down-step every sample, no probe can arm (a dense main is not a blind corner by design), and
the raw law pins the floor:noaudit21.41 ± 0.48 against the 35.8 ceiling. The equilibrium
audit is the only explorer and does the work: the calibration audit confirms a real cold-start
gain at 32% (+3.3pp over the floor while main warms faster under the larger window; the band
is five strides past the floor, beyond its reach) and parks for 32 samples, the park's audit
walks up through the band and confirms at s48 on the peak (six seeds at 56–57%, two at
68–69%), and the loss is a ~48-sample approach at any horizon: 28.04 ± 0.18 at 72 samples (gap
7.8), 32.63 ± 0.30 at 160 (gap 3.7, ~1pp of audit duty permanent). Planted above the cliff the
product does not hold: density slides the window off the cliff at ~49%, the rail returns it,
and on the landing sample the rate recovers by 11pp, whichisWorkloadShiftreads as a
workload change, so the stand-down discards the claim with the window on the anchor and the
slide repeats every ~19 samples with the anchor creeping down (planted at 70% over 160
samples: 29.99, ten vetoes,closed−5.2; at 55%: 27.95). Implemented as the return
cover (isReturnTest): a return's landing and settle samples wait for the retest, which
breaks the cycle (160 samples: 70% 30.06 → 35.30, 55% 26.13 → 35.08, two vetoes each; the
pre-cover baseline sat below LRU on both),novetoreads 33.75 / 30.82 there, and the cover is
bit-identical on the unplanted path, where the anchor is a park and the rail never fires.
Release posture is HOLD on the repeated-real-trace recheck; the planted
synthetic prize remains valid, but the original one-pass gate was not the full price. Class 3
owned by the audit layer, aswhisperis on the window side, with
class 6 on the plants: the C2 arrival-discard shape on the rail's return, which the
re-price found unreachable because the rail never fired across the battery. Rowed as the
main-side mask's sentinel.sidecliff (spec
audit-regret/specs/sidecliff.json, 96
samples): a zipf core (0.15·max, α 1.0) at half of traffic, a near band 300 requests apart at 8%
that the 2% floor already catches in full, and a far band 7,000 apart at 30% caught only from a
55% window. Static, dense-swept: 36.4 at 1%, a 42% shelf from 5% to 40%, 44.4 at 50%, 63.4 at
55–65%, 62.3 at the cap; LRU 61.6. The near band's fixed hits spread over a growing window pin
the average law's rest point at ~14%, sighted and worthless at the margin, sonoauditandreactivesit there (41.85 / 41.80) and the audit layer is the only explorer. The calibration
audit arms at s7 withAuditClock.downstill at its initial value and a stride of room below the
rest, walks down, loses half the near band at the floor, reverses and fails through its own base
(arung32,auditWait32), and the machine then holds the shelf for 33 samples until the
rung-32 up-audit crosses the step at s47 and parks at the cap at s50: 52.51 ± 0.03 on the parent
cell at seeds 1–8 (identical event sequences), a gap of 10.9 that is latency alone (5.32 at 192
samples with the prefix identical; trace seeds 11 / 13 read 8.57 / 9.02). Two sibling
constructions from the other lanes price the shape below the rest:s_faderail(a trickle-pinned
rest at 13%, the down walk wall-sits the floor for fourteen samples and fails at budget, 46.12 ±
0.06 against 55.02) ands_sidebet(a starvation probe finds a mid band at 28% and plants a claim
without a park, so the follow rule has nothing to follow and the calibration audit still opens
down, crashing at the mid band's cliff with a wait of 16; 45.31 ± 0.15 against 53.30). Planted at
the peak the product holds the cap (61.35), so the whole loss is the approach. The opening side
from an interior rest point is the latency-face residual andabsolve's
entry names as a knife edge; this is its stationary witness at the opening rung, with no prelude
and the prize present from the first sample, filed under class 4 with that residual (the
signature reads class 2, walk-paced). The symmetric cells, a prize below an interior rest where
down-first is right (crestpast's drift,cp_w097), are the other side of the same coin. The
guard rail also loses here rather than helping:novetoreads 60.62 against 59.28 on all eight
seeds, and the retest is what bounds the loss, sincenoretesttakes seed 2 to 51.63
(/climber-minimize, §5).jumpslide (spec
audit-regret/specs/jumpslide.json, 130
samples): a hot set of 0.5·max keys replaced wholesale every 13 samples at 52% of traffic, a band
5,600 requests apart at 30%, a scan at 18%. Static, dense-swept: 35.5 flat from 1% to 40%, 50.3 at
45%, 53.4 at 55%, 54.8 at 65%; LRU 49.5. Between jumps the hot set is main-resident and the miss
fraction ~0.4, so the band is caught from ~43%, a residency knife edge (catching it lengthens
residency, losing it shortens it) that collapses in one sample. Seeded 1–8 the machine reads
38.70 ± 2.79,noaudit44.23 ± 4.07 andreactive41.21 ± 3.64, every arm 5–11 under LRU. The
route: the calibration park's 32-sample expiry lands two samples after a jump, the recovery
stands it down and the s42 audit goes down to the floor, so the starvation ladder takes over; a
first-round probe confirms at 43.2% on the stride where the band is first caught, since 4× the
bar is first earned exactly there, at the catch region's near edge, and a seven-stride confirm is
underPROBE_COMMITMENT_DEEP, so no park rule applies and the edge position is handed to density
with a claim planted and nothing held. Density's rest point is below the edge (0.7–1.0% a sample
down); each jump lands the fresh set in the window and kicks it up 7–8%, and the drift returns it
to the edge as the next jump lands: seeds 4 and 8 hold that sawtooth at hr 60–62 between jumps,
while a landing under ~48% lets the drift cross the edge first, the band collapses, density sends
the window to the floor, and the blind corner re-climbs seven strides ofwh0 or a jump crashes
the climb. The rail never fires, since a one-sample collapse inflates the deviation its margin is
priced from,shallowmoat's sentence, with or without jumps. Period 17 reads 5.72 (the calibration
audit goes up instead and parks audit-grade), 26 8.2, 6 / 11 18–20, 40 15.0; trace seed 11 16.7;
260 samples 14.8; a static uniform hot set in place of the jumps 52.27 against 61.63 (the slide
released there by a stand-down on the sample after a capped retreat's landing, which the retreat
cover does not span); planted at 60% the product holds. What the record did not have: a
first-round starvation confirm's verdict position is the near edge of what it found, by
construction, and nothing parks it; class 9's second clause feeding class 1 (the retention
sentence, the parent) and the class-5 sawtooth, with class 4 on the re-climbs. A sentinel row.lowbar (spec
audit-regret/specs/lowbar.json, 96
samples): a uniform bulk over 2·max at 42%, a band 6,000 requests apart at 34% that only a ~68%
window catches, a scan at 24%, and a gap-1 trickle at 0.4% that keeps the window sighted so no
probe arms and every seed takes the audit path. Static: 13.0 at 1% declining to 7.8 at 65%, 32.5
from 70% to the cap; LRU 30.6; a 9% rate. The audit's crash bar isAUDIT_BAR_FRACTIONof the
rate frozen at the arm where that is under the 5pp cap, 1.37pp here, and the calibration walk's
cumulative drawdown along the bulk's decline crosses it at the tenth stride, one short of the
catch point; the tolerant retry's three sub-bar samples run out at 44%, the rung-32 walk's at
63% with its next stride on the plateau. Seeded 1–8: 13.40 ± 7.48, seven seeds pinned at
11.3–12.1 under the 13.0 start (three crashed walks' duty) and one at 26.3 whose tenth stride
cleared the threshold by 0.07pp where seed 1's missed it by 0.20 (trace seed 11: 26.30 / 11.61);noaudit12.72 andreactive12.49 are the terrain's floor at every window short of the catch
point, the same loss as the 17pp under LRU. With the crash fraction at 0.25, or the absolute 5pp
bar, three of four seeds arrive at the first walk (26.3) and the fourth takes the reversal exit.
The moat family's mechanism (the abort at the −bar contour one stride short of the far bank, the
persistence a stride short on the retry) at a fraction-bar dose: the bar set by the rate rather
than the terrain, and a sub-bar region wider than the persistence.s_nearbasinis the same
exit at a 16% rate behind a calibration park (about 7 of its 23; the crossing seeds clear the
level test by 0.02–0.03pp, andnocalrecovers only +0.35 because its own first walk crashes on
the same bar). The first measured cell on which the fraction's level is not a tie (the record
holds it inert at 0.10 and 0.25 on the sub-third holdout and onarc_S3's plateau); its cost
side is unmeasured where it binds (thelowmixrows,arc_S3and the thin-signal floors,cp_w050, a fresh sub-third holdout), and the carve-out the cell resembles is closed (the
split), its deviation floor dead. Narrow by construction: 5,500 apart catches at the
tenth stride, 6,600 passes the cap; at 16384 the band fits at 40% and the cell reads 4.8. Class
4 (reach), the walk's exit pricing; a sentinel row beside the moat rows.moatweather (
/audit-regretround 9; specaudit-regret/specs/moatweather.jsonwith its
dose-matched and peak-level controls, 96 samples, trace seed 11): the calibration audit's value
reverses sign under a level change that lands on its first strides. A zipf over 1.5·max at α 0.95
(50%) fills main, a band 6,400 requests apart (30%) is the prize, an 8% scan keeps probation thin,
and the attack is a fourth member, a pure one-shot scan at 12% of traffic square-modulated at
period 3 samples with amplitude 1, so its share steps between 0 and 24%. Static over 22 windows:
30.4 at 1% declining to 28.8 at 52%, a flat region spanning 1.6pp across 51 points of window, then
a 23pp step to a plateau, 51.80 at 64% and 51.19 at the cap; LRU 50.2. Reaching the plateau from
the floor takes ten consecutive strides. Against the unmodulated control the curve is the same
within 0.9pp over the whole operating region, the same argmax and the same plateau within 0.5pp;
only the 55–58% transition differs, three times the machine's window away and in the direction
that would help a walk. Seeded 1–8 the machine reads 31.20 ± 0.31 against the control's
47.30 ± 0.54, 16.1pp on 8 of 8 seeds. Undosed the calibration audit is the whole answer: it walks
ten strides across the flat region while the rate moves 0.294 → 0.302, catches the band on the
eleventh and parks at 65%, wherenoauditmust first escalate the starvation ladder to rung 64
and does not arrive before s47. Dosed it crashes on its first stride (a 5.57pp step againstmin(RESTART_THRESHOLD, AUDIT_BAR_FRACTION · base)= 4.87pp), its retry's bold driver reverses on
a step and its raw-sample streak confirms at 675 entries on terrain flat within 1.6pp, and that
confirm's bridge write resetsstarvation.rung32 → 1, so the ladder never reaches the rung-64 ×4
walk that crosses.nocheapenrecovers +1.3 of the loss, so the bridge write is the smaller half.
The audit is worth −10.2 / −8.0 / −1.6 on trace seeds 11 / 7 / 13 and +7.5 undosed, with seed 7
bimodal (6 of 8 trapped) and the other two unimodal; the dose is a bar crossing, the audit's value
passing zero in 0.5–0.6 and stepping to −10 between amplitude 0.85 and 0.95 on both instances.
What the mechanism needs is the coincidence, not the noise: the same square wave at period 24
costs −9.4 when its first flip lands on the audit's first stride and +0.1 when a quarter-period
shift moves the flips six samples away, and period 3 is the period that makes the coincidence
unavoidable at every phase. Two controls separate it from the terrain: the scan held constant at
the attack's high level of 24% reads 42.97 ± 0.16 with the audit earning +6.8, and moving the
weather to samples 0–19 alone (21% of the trace) reproduces 79% of the loss while the same insult
movesnocalby 0.1. The second axis is the traverse length: with the band 3,200 requests apart a
first-round walk catches it and the cell is solved (gap 1.4), 4,800 costs 14.4 and 8,000 19.4.
Doubling the horizon widens the loss (33.27 againstnocal's 45.07) and 16384 keeps half. Class 9
(the abort and the verdict both decided by an exogenous step against a frozen reference) paid as
class 4, with class 6 as the secondary channel; owned by the recovery layer, responderWalk.crashBar's audit arm andWalk.isConfirmed's streak.b_moatnoise, the blind lane's
independent construction (a sine modulation of a resident zipf on a gradient terrain), reproduces
the escalation block but not the reversal, since the calibration audit earns +1.7 there. Nearest
recorded: the F1 dose instruments, whose rows sit at LRU parity on shallow bases and whose largest
audit-layer cost wasflashpark's −3.8, andshallowmoat/lowbar, whose terrain this is; what
is new is a cell the machine solves undosed and loses entirely dosed, and a coincidence axis the
record does not price. No arm recovers it: nothing tested exceeds 41.8 against the 51.8 ceiling
under the modulation, so deferring or removing the audit is not the repair.ramp_retest (spec
audit-regret/specs/ramp_retest.json, 72 samples): a zipf core over
0.58·max at α 1.08 and a two-reference band at 0.31·max. The core's weight is 0.4 while the
band's weight rises linearly from 0.0304 toward 0.7296, so an initially valid high-rate anchor
becomes obsolete. At 8192, seed 1's rail returns the window from 4,123 to 1,055 at s24–25
against a frozen claim of 0.8422. The two arrival samples score 0.4032 and 0.3910; at the
completed retest, smoothed rate 0.5939 exceedsclaim − 3·deviation= 0.5038 because the
arrival raised the live deviation to 0.1128. The retest accepts the claim, the window holds
at 12.9% for s28–38, and the s39 audit recovers the band at s44. The pre-return deviation
was 0.0239, which would give a 0.7705 cutoff at that same measurement. This is the return's
own change widening the tolerance that judges it, at the completed retest rather than the
walk's crash bar.
Seeded 1–8: hybrid 61.48 against the best tested static point 68.54 at 26%, LRU 65.43,
reactive 39.28 and noaudit 45.69. Removing the rail earns +5.54pp, while forcing completed
retests to reject earns +2.25pp (2.18–2.30 on all eight seeds). The latter keeps the return
and its observations unchanged through the completed verdict, then releases the hold and
re-seeds the goal metric. It prices that intervention, not the whole gap or a viable repair.
The same intervention earns +3.87pp at 16384 and +4.75 on trace seed 13; trace seed 11 takes
a clean route and is unchanged. A 32-sample stationary prelude preserves the interaction
(+6.57pp), while repeating the ramp twice reduces its price to +0.25pp. This is a finite
recovery delay, not a permanent pin. The measured neighborhood is conditional: at 8192 the
amplitude ladder changes from a correct rejection at 0.86 to a passed retest and eleven held
samples at 0.88; at 16384 that passed-retest route already exists at 0.84. The rail-path cost remains at distances
0.25–0.37; a 56-sample ramp triggers no veto, and the 88-sample ramp at 16384
rejects correctly. A gap-only bisection at much smaller amplitudes instead finds the known
audit-approach cost, with the retest working, and is not this boundary.
Class 6 (an obsolete claim surviving its retest), with class 3 (arrival-induced masking) and
class 4 (the held position waits for the audit); ownerAnchor.retestFailswithRates' live
margin. Nearest recorded:ghostclaim's obsolete return andsidecliff's rail cost, where the
retest limits the loss. Here the existing retest is reached and passes because of the
arrival's tolerance increase. The two size cells are finite-horizon sentinels. No production
rule was proposed or priced against the corpus; unconditional rejection would also reject
the valid returns the rail exists to defend. The best-sample rule adds a basin selector on this
terrain: in an independent construction (round 10's blind lane), two basins identical through s33
split when the recovery audit's first sample read 0.24pp above its second, soWalk.rememberBestparked on the catch region's near edge instead of one stride inside, and the
ramp eroded that edge under the fresh-park shield (parked 56.7 against 73.6).deadpark (spec
audit-regret/specs/deadpark.json, 120 samples, trace seed 11): the moat
terrain the calibration audit solves (a zipf core fills main, a two-reference band is caught
only past a ~55% window; static 30.6 flat to 50% and 55.0 at 60–65%, LRU 53.1), with two
samples of pure scan at s36–37, so s37 is dead. The audit parks at 72% at s21 against a density
law that wants a smaller window. On the dead sampleroutetestshasBlindCornerbeforeanchor.held, and the audit confirm's bridge write zeroedrefractoryLeft, so a starvation
down-probe arms from the park inside its fresh shield (the shield guardsisWorkloadShift
only). The next live sample confirms the stride by average density; a first-round starvation
confirm is not audit-grade, sokeepConfirmedPositionre-plants the claim at 66% with the
smoothed rate the dead samples dragged down and releases the hold, and density walks the window
to the floor. Seeded 1–8: 38.43 ± 0.22 against the no-burst control's 48.90 ± 2.59. An arm
that holds the park on a blind corner (parkblind) reads 50.80 ± 0.34, unpriced on the gate
battery and on the phase-alternation rows where §5's arming guards lost (shiftdead). One
percent of live traffic in the burst defeats the trigger, and after the shield the recorded
stand-down applies instead. Class 6, class 4 in the recovery. Real corpus: one blind-corner arm
from a held park in 10,538 density samples (ConCat), which failed. Constructed-only; a sentinel
row.needle (specs
audit-regret/specs/needle.jsonandneedle_wide.json, 288 samples): a narrow
static peak with a loss on both sides. A loop (a cyclic scan) over 0.55·max at 28% of requests
(period ~16k requests, longer than the window's residency at every window up to the cap, since a
54-58% one-shot scan holds residency near W/0.7 requests, so only main can hold it, by frequency,
and only while main has room: window ≤ 45%), a two-reference band 0.57·max apart caught from ~41%,
and the one-shot scan. Static, dense-swept at 1%: flat within 0.4pp from 1% to 40%, a peak at
44-45%, then ~-0.5pp per 1% of window to 17-21 at the cap; LRU 8-11. The product held at a fixed
window matches the reference within 0.25pp over 21 (cell, window) pairs, 0.12 around the peak. An
audit confirms on four consecutive samples above its frozen reference plus 1pp ending on or after
the walk's fifth sample (AUDIT_CONFIRM_STREAKwithAUDIT_COMMITMENT) andnextStridenever
commands zero, so the machine keeps a peak only where the curve stays above the reference across
four consecutive stride positions reached late enough in the walk. The calibration audit parks at
32.0% on every cell, seed and size, so the park's up-audit samples 38.2 / 44.3 / 50.6 / 56.7 /
62.7%, the first stride lands just under the catch point, and the verdict is one reading: whether
the 62.7% sample, three strides past the peak, clears the bar. The band's share sets that margin
(-5.70 -4.54 -3.67 -2.08 -1.25 -0.99 -0.54 at shares 0.04-0.10, +1.08 at 0.12), a gradient that is
a threshold in the outcome. Narrow (shares ≤ 0.09): at 0.08 the rung-16 audit reads 33.89 /
32.83 / 30.13 against a 28.58 bar, misses on the fifth, and walks on up the far slope to a crash;
the rung-32 one reads 34.11, overshoots, reverses, and re-crosses the peak at 27.28 because its
far-side excursion evicted main's loop, then fails through its base (audit rung 64, wait 128, then
256); at 576 samples the rung-64 one strides 25% across it (57.0% above, 80.2% a crash). 27.15
against 33.73 at 44%,reactive26.98 andnoaudit26.88 pinned as well (the flat approach has
no gradient for either law, so the arms read the flat's rate from different windows),closed≤
0.05 while the gap grows 3.65 → 7.71 with the prize; pinned on 12 of 12 admission seeds (one
decision path: the seeds move sample rates by up to 2.6pp and flip no decision), at 7 of 7
alignments reached by planting the start so the calibration parks at 33-38% or not at all, on
three trace seeds, at 16384 and at 576 samples (27.31 against 34.51). A run of four is collected
and voided where the walk enters the region on its first stride (the band 0.52·max apart:
38.2-56.6% above, 62.5% not), and the same four positions confirm when reached later in a walk, so
the region's width alone does not decide it. A share of 0.10 is a 3-in-12 coin (a third pass
clears the bar by 0.13pp) and is not a row. Wide (0.11-0.12): the audit confirms on its fifth
sample on the far slope and the best-sample rule returns it to the peak, where it parks at the
ceiling's rate; the park's next up-audit is the tolerant retry and rides three below-bar strides
up the far slope, the restored park reads 24.5 against a parked 38.0, and the recovery's second
sample (a +8.6pp first difference) lands one sample afterRETREAT_COVER, soisWorkloadShift
reads the machine's own recovery as a new workload and the stand-down discards a correct claim on
its anchor; density then slides off (its rest point is the floor, main's hits being real and worth
nothing at the margin below the knee) and the starvation ladder cycles near-edge confirms 1 → 64
without a park. 28.56 against 36.37 at 45%,noaudit26.15, andpricedshift34.45: the two
arms are identical in every dumped field until the one sample where the shipped machine stands
down, on every seed checked, so +5.9 is that decision. The same responder fires on the narrow face
at a share of 0.04 and costs nothing, the park it discards being on the flat. At 96 samples the
wide face reads solved: the discard lands at ~s116. At the restored park the deficit is -13 then
-4.5 after the up-audit's crash and -1.2 after a down-audit's (4 of 4 each, parked sd ~0.2, ~10:1
on every seed of both wide cells): class 7's first synthetic reading above scatter, and small by
itself. Robust to the band's distance (0.52-0.62), the loop's size (0.50 flips the narrow cell
wide by moving the knee out, the fifth sample clearing by +0.44), two regenerated traces and 16384
(14 of 14 neighborhood cells at 4.9-8.2). Class 4 in the verdict's geometry (narrow), and class 7
feeding class 6 (wide), both owned by the recovery layer; class 1 is the co-requisite of each. The
narrow face needs narrowness only, the run breaking where the far slope has merely returned to the
flat's level; the far side's depth is what class 7 and the wide face's route need. Nearest
recorded:crestpast, whose far side declines slowly enough for the fifth sample to clear, and
every far-band family, whose plateau runs to the cap where a walk wall-sits for its streak. Round
8'spinnacle(record only; its workspace is gone) overshoots by one stride on arrival, which the
narrow face does not. Constructed; the narrow face's prize is bounded near 8pp by the generator (a
steeper far side lets the window hold the loop itself), and no real instance is claimed. The
ten-point anchor grid has no point between 40% and 50% and under-reads this ceiling by 1.2-3.6pp;
sweep it densely.
Terrain classes, and what reachability means
The families above are shapes that break a climber. These are properties of the terrain
itself, which is a different axis: they classify the hit-rate-versus-window curve rather than a
trajectory over it, and they are what decides whether a constructed trap is worth chasing.
A non-convex window response is a real class, and on it the rest point is the deficit, not the
travel. Sweeping static windows, some cells have a trough deeper than 0.5pp on the way from a 1%
window to their optimum, and several are real traces rather than synthetics. The average law's zero
crossing then sits in the wrong basin: on two deep-trough cells it rests at a 1.4% and a 6.9% window
against 80% and 90% optima, and the rest error is 75% and more than 100% of the whole deficit — one
of them runs above its own static value at that rest point. The unimodal control is what makes
this trustworthy: there the average law rests 1.9pp of window from the peak for zero hit-rate loss.
So "the climber holds a small window because it has not travelled" is wrong on this class; it holds
a small window because that is where its steering law rests. This is invisible from the canonical
published curves, none of which is non-convex, and it is the terrain half of the marginal-steering
thread (§5).
Classifying a large loss corpus by whether a fixed window could have won it: two are
structural (no fixed window wins, a real W-TinyLFU limit), none are ceiling-bound, and the
rest are steering — a reachable fixed window ties or beats the competitor and the climber misses
it. Over 80% of the conceded margin is recoverable inside the reachable window range, so raising the
structural ceiling wins nothing. Terrain is concentrated: most of the steering cells are block I/O,
the family with flat (2–6pp) and sometimes non-convex responses.
Where the real-trace loss goes, by mechanism. /audit-regret's teacher splits the climber's
costly samples (those losing at least 1pp to that sample's best static-window copy) by the mode
the machine was in. Over seventeen real density-tier cells at one seed, each weighted equally, the
split is steering 46%, audits 22%, parks and holds 15% and probes 13%, and the families differ.
On cloud-physics (nine cells) steering is 66%: the average law resting at the wrong window, its
error pointing away from each sample's best window on most costly samples, with parks holding a
wrong window on the w097 family (15–40% of its loss). On the ARC cells, whose optimum is the 1%
floor, the exploration machinery leaves it: audits 33% and parks or holds 27% on S3, audits
37–45% on P1 and P8, and probes 65% on DS1 at 1M and 2M. The two problems want different
repairs, the rest point on block I/O and the exploration price on floor-optimal storage traces.DS1 at 4M is excluded, since its sampled copies score below the climber there.
Do not tune the audit schedule for this class. The equilibrium audit exists for a sighted false
equilibrium, which is exactly this shape, and its cycle costs more samples than these short cells
have — which invites the conclusion that the clock is too slow. The ablation refutes it: againstnoaudit the mean delta is under 0.06pp, the significant cells split evenly between cost and help,
one cell of twenty-two moves more than a point, and the mean flipped sign as cells accumulated,
which is what noise looks like. The layer is neither the cause of this class nor its cure, and
shortening its schedule would buy a measured zero while disturbing a constant calibrated on the gate
battery.
Low audit exposure only bites where the terrain has something to find. This is the rule that
decides reachability, and it is why several deterministic constructed regressions are parked rather
than chased. The stillness-starved traps (posjam_d0, bandtrap2, phases_d050) arm zero or
almost zero audits, and the reachability scans come back clean on every real cell. The nearest real
approach is always the same cell, corda, which calibrates once and never re-audits — the known
blind hold — and it costs nothing there, because its terrain is flat and it already scores within a
third of a point of its ceiling. A cell that never re-audits on flat terrain has lost nothing.
High per-sample scatter and wide terrain are anti-correlated in practice, which is the
structural reason the modulation traps are hard to reach. Every real cell with scatter at trap
strength has under 3pp of terrain; every cell with wide terrain has scatter well below it. The
constructed traps hold both at once, and no real cell in the corpus does. The one real cell carrying
trap-strength scatter on wide terrain has an optimum that moves — which makes its scatter
window-informative, precisely the case the goal-metric layer exists to answer.
LLM prefix-cache traces hold both, and the machine climbs on them slowly (the Qwen Bailian
traces, read by bailian; /climber-gate's prefix-cache row). A sample is a few hundred requests
that each re-read a session's whole prefix, so on Thinking@16384 the per-sample rate swings between
14% and 60%, while the static 1%-to-80% span is 4.4pp there, 6.9pp at 65536 and 18pp on
To-C@185364. The response rises toward the 80% ceiling on every such cell, a window planted at 80%
holds there (Thinking@16384 36.99 against LRU 36.26, To-C@185364 43.97 against the static 43.96),
and the MiniSim leader reaches the 80% value within one pass, so the loss is steering into that
basin, not missing information. The window earns nothing until it spans one inter-turn reuse
interval (about a fifth of the cache on Thinking@16384), so it starts starved, and three things
keep it there. Growing the window demotes protected's coldest entries and hands the window
probation's coldest, so the hot set stays in main and keeps hitting there: a walk to 80% scores
what the planted run does at the same point in the trace, but its density error reads shrink until
the hot set migrates (on Thinking@16384, a median of −3.9 at windows of 40% and up on a seed whose
walks stayed 23 samples there and +0.3 on one that stayed 140, against the planted window's +0.6).
On the static anchor the average law is bistable there, negative from 1% to 40% and positive from
50%, where the window takes over main's hits, so the floor and the ceiling both attract and a climb
confirmed below about 45% is reversed. The marginal form crosses up near 9% and rests near 71%
instead; on Coder@65536 the average law rests at the floor or near 40% (1.1pp under the peak) and
the marginal form at the ceiling, and on the To-B control, whose average law rightly rests at the
floor, the marginal form rests near 13% for 0.2pp. And probes and audits are judged across phase
swings several times the window's effect, so most fail and their backoffs escalate.
Two properties matter when interpreting these comparisons (§1). A prefix lookup stops at a
request's first missing block, so only the leading run of hits is usable, while the simulator
counts every block hit. LRU's usable and block hits were equal in the studied replay; that is
not a universal LRU guarantee. A small window streams a long request through TinyLFU's admission
one block at a time, and a rejected shallow block can strand the hits behind it. The shipped
climber's usable hits run 0.8–2.8pp below its block hits in these measurements (To-C@185364 23.4
against 26.2; 0.5–1.7 at an 80% window). The source study's
workload-aware policy ranks blocks using request
category and turn, idle time and prefix depth, preferring deep blocks for eviction on a priority
tie. These fields can be recovered from the released request records, but the flattened generic
access event and cache maintenance stream do not carry them. That richer input is distinct from
the key-only steering loss above.
A bounded information study did not qualify a generic trigger. Across sixteen cells and eight
sampling seeds, two 64-key banks predicted reuse over the next C block events, where C is the
declared cache capacity, using a fixed learner and an authentic demand-fed aged-frequency sketch. On
the seven primary cells' held-out key Brier, bounded reuse history improved 1.63% relative to age
plus frequency (AF), but lost 0.68% to an equally deep AF-child control with no new feature.
Type/turn improved 0.40% over AF child, below the 1% bar, and failed request-weighted and control
checks. Frequency alone had lower primary key Brier than any tested extension. The observer and its
core learner occupied 31,872 native bytes per replica, excluding the shared sketch and offline
evaluator; no production CPU or JVM-size claim was tested. All six history/metadata extensions
failed the preregistered gate. This is a limit of that finite learner, budget and horizon, not proof
that context or inexpensive inference is useless. Prediction or ranking gains do not establish a
harmful-mode trigger, a window direction, benefit from invoking MiniSim, or a qualified policy
change.
On these traces the large cells' loss is mostly sample budget, and the HBM-size cells' is not.
A pass holds 8.5 samples on To-C@185364, 12 on Thinking@65536 and 49–59 on Thinking@16384 and
Coder@65536, so one pass of a large cell is warm-up: To-C@185364 reads 26.2 against LRU's 45.8.
Replaying the trace lengthens the run, but replaying its keys also lets the frequency sketch
remember every key from the previous pass. LRU reads the same on every pass, yet that memory lifts a
static 1% window by 4.8pp on To-C@185364 and keeps the climber small: under plain replay To-C@185364
closes only to about 6.5 below LRU from the fifth pass, and Thinking@32768 replayed four times reads
32.95–36.97 across eight seeds, an apparent escape lottery. With fresh keys each pass (each pass's
keys mapped onto new ones) the climber reaches LRU or the static 80% window within two to four
passes on every cell (passes 2–8: To-C@185364 41.3, Thinking@32768 38.8, Thinking@65536 41.6,
Coder@262144 53.4, against LRU's 45.8 / 39.6 / 42.3 / 55.4), as the 20-day AgentX corpus, scheduled
at a steady 16 sessions, converges to within 1.8pp of LRU at every size from 8192. The HBM-size
cells climb slowly even with samples to spare: Thinking@16384 reaches LRU's 36.3 only on its fourth
fresh pass, about 150 samples in. Under plain replay the reactive law looked the better of the two
given time (+1.3 on Thinking@32768 ×4, +4.4 on To-C@185364 ×8); on single passes it is −1.7 to +1.2
against the shipped machine, and −7.2 on Coder at 699051.
The tier boundary. At the 4096 switch the discontinuity is the policy, not the terrain: on a
constructed straddle one entry of capacity moves the answer by several points while LRU steps zero
and both single-law arms are continuous. On real traces the sign inverts, so the cost is borne
below the boundary and the reactive tier is the loser there. Half the cliff is not in the steering
law at all — the tier gate switches the sample period along with it. The placement is set by the
worst case rather than the mean, and it currently has a single defending cell; if that trace were
ever judged unrepresentative the interval below the boundary reopens at once.
What the boundary also gates is the layer, and that is worth more than the difference between the
two laws. A terrain whose response is flat then a cliff carries no gradient at all in the region
a climber searches from its start, so no local law can cross it and only the equilibrium audit can —
and below the threshold there is no audit. Constructed at a maximum of 4096, the hybrid, reactive
and noaudit arms are one code path and read ~23 with an 11pp spread across seeds, while the
density machine forced at that same size reads 45.8 ± 0.02 against a static ceiling of 48.2. The
tier gate, not the workload, is worth 23pp there. That cell is constructed and its realism is
unestablished: it bounds what the placement can cost, and is not a case for moving it.
Path-dependent rest points. At least one real cell settles somewhere different depending on
where it starts, and not because it is sample-starved. cp_w050@123038 holds 54 samples; from the
shipped 1% start it settles at a 20–21% window, and from a planted 30% start it settles at 14–15%
for +4.65pp on the same trace, landing 1.6pp off its ceiling where the shipped start lands 6.3pp
off. The trajectories name the mechanism: from 1% the first command leaves the optimum, because at
a 1% window the window is trivially the denser region, so the law commands a large upward step and
the run then oscillates for the whole trace, with both of its audits aborting during low-rate
portions of the trace. From the 30% plant a down-audit instead runs four clean strides,
confirms at a 2% window and parks. The descent machinery is fine here; the ascent out of the shipped
start is not, and the audit layer that should catch it crashes on the trace's own phase structure.
One cell of ten, reproducible in both directions, not traced to a defect. The September 2026 replay
does not support a short-exposure explanation: the first audit's launch and crash both contain
492,152 observations, while the hit rate falls from 48.8% to 4.0%. Granting that first crash the
existing bounded retry saves 1.50pp over the next 64·max requests and 0.99pp over 128·max on
admission seeds 1–8, but the walk still aborts two samples later and returns to the same base.
Suppressing the retry's two extra output strides retains 0.92pp at 128·max. The gain does not
require those strides, but later observations and decisions also change; this does not isolate
a timing-only effect or demonstrate a false rejection.
The same delay loses on the cp_w081 and planted shallowmoat controls. A pre-existing rate
decline at arm does not distinguish the useful continuation from costly ones after invalidation;
no production rule was established. These are finite-horizon, request-indexed replay comparisons,
not a generalization to unseen workloads or a change to the crash policy.
Descent has to be measured deliberately, because no natural row exercises it. Every gate row and
every real-corpus cell starts the cache where the product starts it, at a 1% window, and on the
frequency-favorable traces that is already the static optimum — so those rows pass without the
climber having to move, and a machine that could not descend would read clean on the whole real
corpus. Planting the initial window is the instrument. Measured, the descent is the density law
unobstructed at 4–6% of the maximum per sample: the log-ratio error saturates near 1.5–2 nats
even at a badly wrong window, so the step never approaches its 30% cap, and a full walk down from
80% costs 13–16 samples. Recovery is therefore a function of the sample budget and nothing else —
replaying a trace four times takes an 80% plant from −16.05 to −3.63. Against the reactive law the
density tier is the better descender (its period is 4× the maximum against reactive's 10×, and its
step is proportional to how wrong the position is rather than a fixed decaying 6.25%), though
reactive holds the higher level at the shipped start on these cells. Two observations recorded
alongside: a failed probe undoes a correct descent in one capped move, which is deliberate and which
the cells absorb; and what a plant prices is the walk back after a regime change, which is tens of
millions of requests for a large cache.
4. The shipped design (the probe machine, >4096)
Non-starved samples are the pure density step. The additions:
Starvation bar: a region is starved when its sample hits <
max(4, requestCount >> 10)
(0.1%, floored at 4 — the floor binds whenever the sketch's entry-denominated cap makes the
sample shorter than 4096 requests).Blind corner gate: probe only when the starved window is small (window starved at ≤max/4 →
probe up) or the sample is dead (both starved; direction away from the nearer bound, deciding
at half). A starved main beside a large window arms nothing: the equilibrium audit owns that
terrain (the upper-corner probe, main starved with window ≥3·max/4 → down, was deleted
§5's corner entry; thecornerprobeharness arm restores it). A large region
earning nothing is visible to density and must not trigger a probe (probing "for" a
scan-filled main destroyed corda_large).
The gate outranks the goal-metric branches, which is right for the guard rail (it adjudicates a
shortfall on the starved sample) and was wrong for the audit (it adjudicates over the samples
that follow): a blind corner that never clears served its whole refractory motionless while the
clock said the position was due, sincerefractoryLeftis armed by a starvation walk'sundoProbeand decremented only inside the hold (an audit's undo left it alone from
before that every undo re-armed it, which deferred the corner's next probe by the
whole rung after an audit that was not the probe's doing). A due clock now pre-empts that hold, a sample the machine
was otherwise spending on nothing.The walk: bold-driver seeded at the 6.25% restart magnitude in the probe direction,
scaled by the refractory rung — ×2 at rung 32, ×4 at rung 64, capped at the 30% max step
(a bound the current tables sit under — ×4 · 6.25% = 25% — so it binds only under retune)
(2026-07 study: deep rungs previously bought permission to walk deeper but not speed, so
escapes crept; rung-scaled strides punch through deep stray walls the flat seed stalled in —
straywall2 +5..+11 across seeds and scales, d050 escapes fuller, corpus 205/205 ties since
natural workloads never reach the deep starvation rungs — the audit ladder shares this ×4
stride scaling and is routinely deep on real traces (audit.rung = 64on 5 of 12
real density cells,auditWait = 128on 4), so an argument from rung unreachability must say
which ladder it means; the full return command on failure is unchanged, which is what
separates this from the rejected v7 travel-budget family);
direction flips only on |ΔHR| ≥ the reversal bar, which is not the crash abort's (see the
crash-abort ending): priced for starvation probes, and for auditsmin(5pp, AUDIT_BAR_FRACTION · max(baseHitRate, noiseBand))— so plateau crossings survive
workload jitter.
Walks honor the 2% floor (via an else-if after the reversal check, so an undo-to-base wins over
the floor clamp), and carry a sample budget of 16 inWalk.samples, which counts up from
the arm (it once borrowed the refractory countdown; the two uses never overlap, but
sharing one field made the walk's depth an inverted expression at each use and forced both
oracles to bound it by mode) so no walk is unbounded — the review proved the crash veto alone cannot
bound walks (below a 5% base rate the absolute form was unsatisfiable, whichAUDIT_BAR_FRACTIONnow
covers, and it stays blind to damage under its own bar). Two
subtleties the code carries: the walk's anti-freeze re-seed (|step| < 2re-seeds toward the
probe direction) cannot be reached by decay in the density tier — the seed is ≥6.25% of a >4096
maximum and sixteen 0.98-decays cannot fall below 2 — but the floor clamp assignsstep = min(-0.0, floor − window)directly (the negative zero keeps a wall-blocked restart
striding into the wall rather than flipping upward off positive zero's sign), so a
floor-blocked down-walk zeroes its step, re-seeds into the wall each sample, and is
re-clamped, burning budget in place (the stall note under State); and
only samples routed to the blind-corner refractory hold decrement its countdown.
A due audit takes the sample without decrementing it. The hold returns without a steering
step (only the below-floor lift, item 7 — one definition,Reading.atLeastFloor, shared with the steering step so the two cannot drift apart). It used to fall
through to the density arm, which is what the dead-phase rider exploited — a handful of
window hits in an otherwise blank sample authorized the maximum step while the probe
machinery was backing off.Walk endings (each maps to a verified failure without it):
A failed walk's return commands sum to its distance from the base. Applied movement can finish
short when a new sample replaces an unfulfilled transfer; later walks use that landing as their
base. This approximation is retained after pricing reconciliation.- Crash-abort (hit rate ≤ probe-start − the walk-interior bar): undo to the probe's start.
Starvation probes re-arm their refractory; audits leave it unchanged. The first crash keeps
the owning ladder's rung unchanged because an exogenous phase shift can resemble probe
damage, and increasing backoff would delay re-exploration. Consecutive crashes escalate that
ladder atPROBE_CRASH_ESCALATION. A starvation probe's bar is priced against the workload's own
scatter —min(max(5pp, 3·rateDeviationEma), 15pp), the adv3 study: the fixed 5pp sat
below real per-sample noise and severed the blind corner's only exit (the dosed mixture
trap crash-cycled one stride off the floor for half its run, −3.4 below LRU, healed to
+4.8 over by the pricing), while the 15pp cap keeps the abort's second job — bounding a
damaging walk's excursion — after uncapped pricing let walks roam the all-blind families
at a measured cost (metronome −0.6, recovered by the cap). An audit's crash abort keeps the
absolute 5pp: a priced audit bar lets audits survive to confirm and park more, which is
the R4-F1 amplification dial (zigzag −10.5, widepin below bar, demoflood −5.2 underdevaudit). Its depth stays absolute, but
the threshold is a level test against the rate frozen at the arm, so where the whole rate is
smaller than 5pp the test is unsatisfiable and only the budget bounds a walk doing real
damage (arc_S3: 0.0442 → 0.0216 while the window marched to the structural ceiling).AUDIT_BAR_FRACTIONof the starting rate floors it there. That is not a depth pricing —
every dead candidate widened the bar so walks survive longer — and it binds only below a
third of the rate the threshold prices, leaving every family in the graveyard untouched.
The reversal does not share this bar: the two exits read different
statistics, so the crash abort is priced by the level alone and the reversal byAUDIT_BAR_FRACTION · max(baseHitRate, noiseBand).AUDIT_BAR_FRACTIONtherefore carries two
jobs, capping one exit and pricing the other, and the two share a fraction without sharing a
bar. - Reversal-through-base: a bold-driver reversal that would cross back through the probe's
own starting window found nothing — finish as a failed experiment (undo + ladder ×2) instead
of walking out the other side (a ±5pp blip once converted an up-probe into an unterminated
down-walk that pinned the window at a single entry). - Adjudication, when the watched region earns ≥ 4× the bar AND the walk has taken at least
its committed depth (see 5). An up-probe confirms iffln((windowDensity+ε)/(walk.baseProbationDensity+ε)) > 0— the probed window must out-earn
main's MARGINAL rate, the probation density frozen when the probe armed — while
down-probes keep the average-density test (error·dir > 0; the window has no marginal
substructure to price against). Why marginal: climbing donates only the protected
allocation, whose squeeze demotes into probation and displaces probation's coldest entries,
so probation is the boundary a window grow actually taxes; adjudicating against main's
AVERAGE (the rejected simpler form — four simpler verdict shapes were measured, each trading
a family, see §5) vetoes genuinely-winning positions because the protected core dominates it
(ablation: trickle 56.1→69.5/73.2/73.1 across seeds, bandtrap2 69.05→72.55, widepin
45.5→57.7, sample-aligned phases 46.9→60.1). Why frozen: the walk's own demotions enrich live
probation (a protected-saturating hot core demoted mid-walk earns furiously there), and live
adjudication is an ABSORBING false-veto (demoflood: 58.94 pinned with zero confirms vs 67.17
frozen) — the freeze's own cold-start-transient baseline is bounded and self-heals because
every re-arm re-snapshots. Confirmation keeps the position and resets the ladder to 1
(control passes to the density arm in the SAME sample); anything else fails (undo + ladder
×2) — neutral verdicts must fail or density walks the window home and the probe refires (S3).
A zero baseline (probation earning nothing at arm) means any ≥4×bar earnings confirm —
intended, the opportunity cost of a dead boundary is ~zero (nullchurn stays harmless). The
named trade, priced and journaled: on low-HR bistable shapes whose probation concentrates the
reuse band while an LRU-ward escape needs a diluted start (lowmix family, 2–7% HR), the
frozen baseline false-vetoes an escape the old diluted-average verdict luckily confirmed —
v5a lands the bad basin 3/3 seeds vs ship 1/3, with ZERO real-trace echo across the 32-cell
defended set. The walk's exit bars are settled: a starvation
probe's exits are priced against the live scatter, the audit's crash abort keeps its
absolute depth capped atAUDIT_BAR_FRACTIONof the frozen rate, and the audit's
reversal — a first-difference test, not a level test — takes the same fraction of the
larger of that rate and the noise band; every widening of the crash abort is dead (§5).
Four simpler verdict forms were measured and each traded a distinct family — see the
local study archives. - Budget expiry: failed experiment (undo + ladder ×2).
- Crash-abort (hit rate ≤ probe-start − the walk-interior bar): undo to the probe's start.
Escalating commitment — the resolution of the stray-exit dilemma. Stray and transferred
hits scale ~linearly with the region's size while their density does not, so an absolute exit
ends deep walks a stride short of a dense reuse band (making that trap absorbing), while
removing the exit re-creates deep-walk displacement on thin-signal floors (w50/S-class, real
traces). Neither rule wins both — four rule families were measured (§5). The shipped answer:
first-round probes exit cheaply (commit 0), and each adjudicated failure lengthens the ladder,
whose deeper rungs commit the next walk past the stray zone before the adjudication exit may
fire (length 32 → commit 2 samples, 64 → commit 10). Confirmation is NOT exempt from the gate:
early confirms on false mid-depth bands reset the ladder and neuter the escalation (measured:
the long-trace escape fell 53.3 → 38.7 with the exemption). Short traces never reach the deep
rungs, so thin-signal floors keep the cheap behavior exactly; steady deep-band traps escape by
~round 3 (≈100 samples) — an absorbing pin becomes a bounded, temporary dip (demonstrated:
mixture_d050 scores 33.5 on a dip-length trace and 53.3 on a 3× trace as round 3 escapes).
A confirm the density arm reverses in the same sample also deepens the ladder:
the confirmed position is walked home at once and the corner re-arms, so a reset there restarts
the ladder on every cycle of a dither that never reaches the band it is looking for. That was
the majority ending of a starvation confirm (668 of 881 across the battery and corpus dumps),
and pricing it as the completed experiment it is closes the reach half ofshallowmoatand liftsbandtrap2+4.1,trickle_s7+2.8 andtrickle_s11+0.9 (seeded, 8 of 8 on each),strad_p8@4097+1.9,widepin+1.7 (7 of 8) andphases_d050@32k+4.8 (4 up, 4 unchanged);phases_d050+0.3 with two seeds falling to its low basin, the N=8 mean above its bar. The
confirm's handoff and its zero refractory are unchanged, so nothing is undone and
the escalation is fast: rung 64 in ~10 samples on a steady dither. Its price is on the
thin-signal floors with a dither in a low phase, where a deep walk armed in a trough has no
crash abort (its base is the trough, §5's v7):arc_DS1@1051635−0.7 on a 10-sample trace anddeadphase−0.2, accepted (§5 has the guard that removes both and
what it costs), and onnorank_rep_r6one seed of eight (41.1 → 20.3): a reversed confirm on
a rewarded ladder deepens it, so the fail that follows waits 16 samples where the reward's
ladder waited 2, and the ×1 walk that reached that seed's escape re-arms 25 samples later.
A reversed confirm at the deepest commitment that the goal metric confirms (the audit's own
streak and beat-base test,Walk.isAuditGrade) is an audit in all but name and is kept as an
audit's confirm is: it parks. Any other starvation confirm still hands to density,
so the cheap re-probing that phase alternation relies on is untouched. The park's own audits
are covered by it: a crash-scale move while an audit walks out of a park is what that re-test
produces and its ending returns to the park, so it does not stand the park down
(isWorkloadShift); a shift with no walk in flight still does. Measured on shallowmoat, where
a deep walk finds a band density then dismantles (36.4 on the wedge seeds against 30.7), and
onflood_j100(+1.1 at N=8); bit-identical on the rest of the seeded battery and corpus.
A kept confirm at or short of the farthest window the ladder's walks have already confirmed is
a repeat: the machine found that ground and lost it, so a walk that only finds it
again has not earned the reward, and the confirm keeps its handoff and its zero refractory but
deepens the ladder as a reversed one does. The ladder remembers the farthest confirmed window
per direction (Ladder.remember/isRepeat, kept and reversed confirms alike) and forgets it
when a walk fails or crashes, since a walk that keeps nothing shows the terrain it was found on
has moved. Onabsolve_p8the reward had pinned the rung at 1 on every lure period (a
first-round stride catches the lure's +5pp shelf, density dismantles it when the lure stops, the
corner re-arms); with repeats priced the ladder reaches 64 in six periods and the ×4 walk
crosses the valley in two strides, parking as an audit-grade confirm at the top: 27.95 → 46.29
at 8 seeds. Bit-identical to ship on every other cell of the seeded battery (64 cells at seeds
1–2, the movers at 3–8) and the corpus (arc_ConCat−0.03 over four seeds, noise): the census
of the shipped machine (15 cells × 8 seeds) found no kept confirms at all onbandtrap2,trickle,deadphase,strad_p8andshallowmoat(all wedges), and the kept confirms onrep_r6,widepinandphases_d050are followed by crashes, which forget. The memory is not
cleared on the anchor's discard: the confirm sample is itself the lure's on-step, and where the
walk lands at the previous anchor that step discards the anchor on the very sample the confirm
is judged (measured 41.4 against 46.4 onabsolve_p8seed 1).Refractory ladder: length starts 16 and doubles per adjudicated failure to 64 (the arming
doubles first, so effective waits are 32 → 64; after a kept confirm the next failure waits 2;
a confirm the density arm reverses doubles the rung without a wait).Below-floor lift: the density arm's clamp raises a sub-floor window to the 2% floor (the
initial window is 1% and otherwise wedges under the documented floor). The blind-corner
refractory hold honors the lift too — a workload whose every sample is blind never reaches
the steering step, and the bare hold wedged the initial 1% window for the life of the run
(adversary F4, metronome; enforcing the floor there costs the one thin-signal
cell that was enjoying the wedge its documented price, w50 −0.5pp, siblings unmoved).setMaximumSizeresets the probe state, the pendingadjustment(a probe-scale residue
must not crush a fresh partition), andsample.previousHitRate(a baseline earned under the old
geometry inverts the seeded direction of the first sample under the new one, at
restart magnitude), and the sample period ismin(saturated 4·maximum, sketch.sampleSize)— the sketch's sample is entry-denominated, so
weighted maxima cannot inflate the period by the mean entry weight (a weight-units period froze
weighted adaptation entirely), and a near-Long.MAX maximum cannot overflow it. The sketch's
sample can sit far below the maximum whenever the sketch was sized for something smaller: aninitialCapacityhint sizes it at construction, and growing the maximum leaves the old sample
in place until the lazy half-fullensureCapacity. The period then runs well below 4·maximum
while the steps already scale to the new maximum.That used to be accepted as a transient that heals when the cache fills. The period does heal;
the window it drove to does not.climb()therefore skipsdetermineAdjustmentuntilweightedSize() >= maximum() >>> 1, the same half of the maximum that sizes the sketch,
instead of adapting the moment the sketch exists. A cold cache is not evicting, so its sampled
hit rate does not describe the region split; before the gate, the compressed cadence read that
signal often enough to walk the window across the partition. The cold branch discards a full
sample rather than resetting every cycle: resetting every cycle is not available, since several
pins record into the sample on caches that never fill, one of them
(asyncBulkCompletion_doesNotRecordAccess) on ten in-flight futures that weigh nothing until
they complete, so no maximum both opens the gate and holds the entries. Leaving the sample
untouched is not available either, because it then grows without bound and breaksLocalCacheSubject's mirror of the period gate (HashClashTestreached 233,364 requests
against a 2,572 period).WindowClimber.discardSamplecloses it on the same perioddetermineAdjustmentuses, so the invariant holds unchanged and the cold prefix a first real
sample can inherit is bounded by one period. Pinned byBoundedLocalCacheTest.adapt_partiallyFilled_holdsWindowand itsadapt_filled_climbsWindow
pair.Priced 2026-09-08 against
product.Caffeine, hit rate %,maximumSizewith aninitialCapacityof 1000 and of 10000 beside the un-hinted arm:arc/S3@400k29.27 / 40.59
against 41.04,arc/DS1@1M8.76 / 8.97 against 12.48,arc/P3@100k33.50 / 39.63 against
40.43;metaCDN@250k,wiki1190@250k,cs@8192andcorda@8192unmoved. The loss tracks the
compression ratio0.4 × maximum / hintrather than the hint, so 16× is free, 40× costs 3.4pp
and 160× costs 11.8pp, and no cell atmaximum8192 can show it becauseMIN_SKETCH_SIZE
floors the sample at 2560. With the gate every hint arm lands within 0.4pp of the un-hinted
one. A patchedsamplePeriodreturning4 × maximumunclamped recovers the same loss and is
inert un-hinted, which is what separates the cadence from the sketch's own density; the two
sharesampleSizein production, so no public API separates them. ThesetMaximumgrowth arm
measured the same −3.4pp before the gate and is closed by it.
Shape of the code: determineAdjustment gates on the tier's sample period, dispatches to the
bound tier, and closes the sample; the density tier's route is a router — handed oneReading (the sample's derived view: both densities, the rate and its change, the starvation
bar, the geometry every branch clamps against), it advances the always-on observers, then
dispatches down the priority lattice to one named branch method per outcome. Each branch owns the rationale that used
to sit inline as a comment block, and the conditions are questions (Reading.hasBlindCorner,AuditClock.isDue, isBackingOff, Walk.shouldCrashAbort, Walk.isConfirmed,Walk.canAdjudicate)
rather than expressions needing a comment to read. An ending is priced by the layer that owns it:probeEnding asks the crash question of every walk and then hands to auditEnding orstarvationEnding. Reading order for the whole machine is the router first, then the branch it
sends you to; sectional comments (Exceptional Scenarios, Probe Walk) carry that order inside
the density tier, and the nested types read in dependency order: the Climber base class →Sample → Reading → Step → the two tiers → Walk/Ladder → AuditClock →Anchor/Rates → ProbeEnding. Behavior lives with the state it reads (Beck's tests, applied mechanically in the
2026-08 refactor sweeps); repeated quantities have one definition each — Ladder.stride (the
rung-scaled stride the walk and the audit's room rule both read), Reading.maxStep,Reading.upperCorner (the wall line that the blind corner, the probe direction, and the
audit's direction rule all test), Step.restartMagnitude, Reading.cappedStride, andsample.hitRateChange (the cross-sample difference the reactive law, the walk's bold driver,
and the stand-down's shift trigger all steer by) — because two spellings of one quantity is how
the room-rule drift happened.
State: fields on the package-private WindowClimber (reached through the climber field thatAddMaximum generates), eviction-locked, cold path only. The climber itself holds the
observation state (Sample), the carry-over adjustment, and the bound tier. The tier owns
its Step; everything density-only lives on the DensityClimber that tier names. The flat fields
are grouped into small objects — Sample, Step, ReactiveClimber, DensityClimber, Walk,
two Ladders, AuditClock, Anchor, Rates — each carrying the behavior that reads only its
own state. The
grouping is the write-owner map made structural: each row below is one object, so a cross-layer
write has to name the other owner to happen at all (landed object-by-object, each stage verified
bit-close against the pre-refactor tree by the paired battery). The tuning constants live inside
the class that reads them; the one exception is RESTART_THRESHOLD, kept on WindowClimber
deliberately because four mechanisms in four classes read it (the reactive restart, the walk's
audit-side bars, the stand-down's shift trigger, the audit direction's stale test) and giving it to
any one would misstate who owns it. Where a constant has one owner but a second reader, the second names it
(Rates.VETO_MARGIN_MIN floors the audit confirm; the fresh-park shield isAuditClock.AUDIT_WAIT_INITIAL long), which keeps the sharing visible.
The tiers are types, and the density tier carries its own supervisor. Both extend theClimber base class (climb over the open sample, and samplePeriod), and a (re)size constructs
the one its maximum falls in: DensityClimber.appliesTo is the tier boundary, written once inresized, and construction is the reset, so a reactive-tier cache never allocates the density
machinery. ReactiveClimber holds the regime predicate, step rule, decay and period;DensityClimber holds the proportional step and its period, and with them the probe machine, the
audit layer and the anchor, the exception machinery §2.1's supervisory switching arms when its
law cannot be trusted. Each strategy chooses its initial direction; Climber constructs its Step
from that direction and the maximum, so replacing the tier also replaces and re-seeds the step.
The two laws keep different verbs on purpose (the reactive one climbs, the density one steers),
and the two bold drivers (ReactiveClimber.climb andnextStride) stay separate: same algorithm, but their bars, reversal conditions, and return
conventions differ, and §4/§5 record each difference as load-bearing.
Declined refactors, decided rather than deferred: no WalkMachine and no method object for
the router — the branches write refractoryLeft, undoRemaining, walk, the anchor, the audit
schedule and the other layer's ladder (the audit-confirmation exception), so either extraction
needs a back-reference and would move the cross-layer writes out of the class that owns them.refractoryLeft cannot join Ladder (the shared type would give the audit's instance a
permanently dead field), and undoRemaining deliberately outlives the walk, which is why it is
a density-tier field rather than Walk state.
The write-owner map — which layer's endings may move which fields — is the boundary the
H4-C1/F2 repairs restored; keep new writes inside their owner:
| Owner | Fields | Notes |
|---|---|---|
| Observation | sample (a Sample: hits, misses, windowHits, probationHits, previousHitRate) |
the counters are zeroed at sample close; previousHitRate deliberately keeps a different lifetime, since it is the memory ACROSS samples that the reactive climber's direction and the walk's bold driver compare against — close(hitRate) carries it forward while zeroing the rest, and only reset (a resize) discards it. WindowClimber keeps thin recordHit/recordMiss/resetSample delegates because BoundedLocalCache calls them on the read and write paths |
| The active walk | walk (a Walk, null while none is in flight: ladder, isAudit, down, baseWindow, baseHitRate, baseSmoothedRate, baseProbationDensity, samples, belowBarStreak, aboveStreak, beatBase), undoRemaining |
one walk at a time. It is an object rather than eleven flat fields, so "dead state while not probing" is the absent object rather than a comment, and a reader must hold a walk to ask it anything — armProbe is the complete constructor, endWalk clears the field, and the router keeps the ended walk in a local because the undo that prices it still reads the bases. The bases are final: frozen-at-arm is the property the verdict studies keep re-deriving (§4's "why frozen"), so the compiler now holds it. walk.ladder is the arming layer's ledger, which makes "an ending may only deepen the machine that produced it" a reference rather than a lookup; refractoryLeft is the starvation refractory alone and belongs to the row below |
| Starvation retry | starvation (a Ladder: rung, crashStreak), refractoryLeft |
moved by starvation endings only (an audit confirm cheapens starvation.rung and zeroes refractoryLeft, the one journaled bridge write, spelled out at its site rather than hidden behind Ladder's methods; an audit's undo leaves the refractory alone) |
| Audit retry + schedule | audit (a Ladder), auditClock (an AuditClock: down, waitSamples, stillSamples, lastWindow) |
moved by audit endings and the position-stillness clock only. The clock owns tick/isDue/restart/reset, so the stillness rule (a moving sample decays the run, it does not zero it) lives with the counter it governs rather than in a climber method. reset preserves the preferred direction, but a resize constructs a new clock with down = true; direction alternation does not survive the resize. AuditClock.reschedule is handed the audit ladder's rung by the ending that prices it, so the clock never reads the other layer's ledger itself |
| Goal guard | anchor (an Anchor: window, rate, held, freshLeft, returning, returnLeft, retestClaim, settleLeft, shortfallStreak), rates (a Rates: smoothed, deviation) |
anchor/park/veto authority and the rate references. Anchor is the memory and its defense in one object because the layer's three invariants run between those parts, and it now holds them by construction rather than by assertion: a shield lives and dies with its park (park/hold/release are the only writers — an audit's confirm arms a shield, a rail veto holds without arming or spending one, since the shield's clock belongs to the confirm that armed it), a park defends only a planted anchor (discard takes the hold with it), and a return implies the park that follows it (beginReturn arms both). isAt/isAwayFrom give the band test one definition instead of three inline copies, and they are deliberately not each other's negation — an unplanted anchor is neither at nor away, and there is no claim to veto against. Rates owns the EMA pair and the two bars priced off it — noiseBand is the three-deviation width, vetoMargin is that floored at VETO_MARGIN_MIN — so the rail's margin and the starvation probe's walk-interior bar read one definition instead of recomputing VETO_MARGIN_SCALE * deviation apiece. The deviation is read LIVE, and the audit's confirming streak is deliberately not priced off it; both notes live on noiseBand itself. A stand-down that discards the claim re-seeds the pair (below): the event that invalidates a claim invalidates the reference the claim would be re-planted from, and the two are one layer's state |
| Motion out | tier (the bound Climber, owning a Step: size), adjustment |
the single per-sample command. step.size and adjustment have different owners and lifetimes: adjustment belongs to WindowClimber and is drained by BoundedLocalCache across maintenance cycles as the transfer carry-over, so it changes at the cycle rate while the strategy's step changes at the sample rate. A resize constructs a new strategy with a freshly seeded Step |
An audit confirmation is the exception to retry-state ownership: it sets starvation.rung to
one, clears starvation.crashStreak, and clears refractoryLeft to allow cheap re-probing.
A reset ladder carrying a live crash streak would re-escalate on the next crash. An audit undo
preserves the starvation retry state, including its current refractory countdown. Re-arming
that countdown would defer the next starvation probe for an audit's failure, as pinned byWindowClimberTest.undoProbe_auditRetreat_leavesTheStarvationRefractoryAlone.
Starvation endings leave auditClock, audit.rung, and audit.crashStreak untouched;
audit endings never deepen starvation.rung.
Each layer's rung and crash streak live in its own Ladder. Apart from the audit-confirmation
exception, endings change only their own layer's retry state and schedule. This includes
non-crash endings: an audit's budget expiry leavesstarvation.crashStreak, and a reversal-through-base leaves whichever streak it does not own
(pinned by audit_budgetExpiry_leavesTheStarvationLedger andwalkStep_reversalThroughBase_leavesTheOtherLayersLedger). Both sites previously cleared
both streaks, which disarmed the other layer's two crash responses — the escalation atPROBE_CRASH_ESCALATION and, for audits, the AUDIT_CRASH_PERSISTENCE tolerance that arms
at a streak of one. A starvation probe ending between two audit crashes therefore restored
the one-sample abort that the moat and the H4-C1 pulse train specifically need it not to
have: the crash-semantics fix was reachable around by an interleaved blind corner.setMaximumSize additionally zeroes the sample counters and re-seeds the step direction; it
drops the walk, which is how the direction and bases that used to linger as dead state are now
discarded with it. A deep walk that reaches mainProtectedMaximum == 0 stalls (transfers have
nothing to take) while its budget keeps burning — measured 7 of 16 budget samples at the ceiling
in one blind-review round; benign but known. The floor-side mirror exists and rung-scaled strides
sharpen it: a rung-64 down-walk reaches the 2% floor in ~4 strides and spends the remaining ~12
samples standing still before budget expiry prices it as a failed experiment (2026-07 post-ship
review). Adjudicated as intended: the wall IS the probed extreme awaiting adjudication (main
earning ≥4× the bar there confirms), the budget must keep burning or a wall-sit is unbounded,
wall samples counting toward the commitment depth is what lets a wall position adjudicate at all,
and expiry pricing stays uniform because ladder escalation on walled families is load-bearing for
the deep-band escapes. That adjudication covers walks whose wall-ward direction is forced (a
starvation probe's direction comes from the starved region); an audit chooses its direction,
and auditDirection refuses one with less than a full stride of room — a window in
(2·floor, floor+stride] used to be sent into the floor with sub-stride room by the first
interior toggle, clamping on the entry stride and burning 15/16 budget samples motionless
before the information-free expiry doubled both the refractory ladder and the audit clock
(adversary round, F2: mixture d025@32768 spent 54% of the trace at its pin; the
room rule is worth +8.1/+9.7/+11.7pp on the mixture-audit family with every other gate row
unchanged). What was a real bug — a restart-scale improving sample at the wall
striding upward off positive zero's sign — is fixed by the negative-zero clamp and
regression-tested in WindowClimberTest (the wall-sit scenario replays the review's repro).
Why the tiers stay: the hybrid-all rejection at cs@563 (−1.27) rules out density at every
size, and the D2 study closes the weaker question of where the boundary belongs:corda's own tier crossover sits between 2048 and 4096 (density − reactive reads −0.61 / −0.86 /
−0.50 / +0.43 at 512 / 1024 / 2048 / 4096), so the threshold is within one octave of the
sensitive trace's crossover even though the aggregate crossover is between 563 and 1024. The
placement is set by the worst case, not the mean (§3, the tier boundary). The weighted threshold
also remains in the weigher's units; the tested entry-count alternatives
did not establish a reason to change it.
The goal-metric layer (the F4 answer; shipped, then hardened).
The probe machine's trigger taxonomy is starvation-only, and a workload can hold the density arm
at a sighted false equilibrium — unstarved, earning enough to be believed, and wrong (F4:
whisper's trickle keeps the window sighted; the mixture family re-pins quietly at scale). The
layer that closes it: an equilibrium audit arms the walk machinery from a position held
still for AUDIT_WAIT_INITIAL samples — the clock counts positional stillness only, decays by
one on a moving sample rather than resetting, and its schedule is owned by audit endings alone;
the FIRST audit after a (re)size is a calibration probe at AUDIT_WAIT_FIRST = 4 — and is
judged by the goal metric, AUDIT_CONFIRM_STREAK consecutive raw samples above the
reference frozen at arm plus the one-shot auditBeatBase gate, because density holds the
equilibrium under test and would veto every walk away from it. A confirm plants the anchor
and parks without a parting density step, shielded from crash-scale weather for one initial
audit wait; the guard rail vetoes a noise-cleared sustained shortfall back to the anchor.
The margins are deliberately split (rail 3·dev; confirm run-length plus beat-base). Each layer
owns its ladder, crash streak and schedule, with the audit-confirmation exception described above.
Audit crashes price persistence in time (AUDIT_CRASH_PERSISTENCE on the retry) rather than bar
depth. A park's first audit follows the walk that confirmed it: the confirm ends a
walk on evidence of improvement rather than its exhaustion, so the ground beyond is unexplored
while the ground behind was just covered, and the alternation was sending that audit back through
it (absolve p16: down from the 32% park into the lure's knee, a crash, the undo's arrival
discarding the anchor; shallowmoat basin B and veilmoat: down from the calibration park toward
the floor it came from). It follows only while the claim stands, the park still held at the arm
and the smoothed rate within a restart threshold of its value at the confirm
(AuditClock.settle/chooseDirection); a rate that moved that much with the window still says
the workload moved and the walk's direction says nothing about the terrain. Unguarded, the rule
swept climbtrend_up's trend misconfirms wall to wall (−0.9 at N=8, its sentinel: the 6570 park
starves main of the growing hot set) and re-phased whisper_mod_p6's re-escape (−1.76, its
sentinel), and on moat_h5000 its far-slope walk (five strides down a 1.5pp-per-stride decline
before the 5pp bar trips) made the next audit the tolerant retry, whose undo evicted the band
(−1.75); the guard returns all three to ship. It also fires, incidentally, after a step confirm
whose smoothed rate at the confirm still reflected the walk's path (moat_h3000/h4000,flood_j100 on seven seeds of eight, shieldtrap_s13 seed 1), where the continued direction had
been a coin flip (moat_h3000 +1.6, h4000 +0.8, h5000 −1.75, flood +4.1); those stay on the
alternation. Priced at N=8, arms rotated: absolve 24.77 → 41.46, absolve_x4 36.45 → 46.89,veilmoat +3.7, shallowmoat basin B +2.7, flood_j100 +0.81 (one seed +6.5), saw_p40 +0.77
(8 of 8), whisper_mod_a12 +0.24, shieldtrap +0.2, scarburst +0.19; mixture_d010_long
−0.27 (8 of 8), demoflood −0.16 (8 of 8; the calibration audit's confirm overshoots the walk's
own best sample and the continued audit wastes two samples before the crash returns it),whisper_mod_p12 −0.05; the corpus cp_w050 +1.07 and cp_w044 −0.17 on one seed each, the
rest bit-identical. Alternation resumes after that audit. Invariants and don't-harmonize warnings
live in rules/design-decisions.md.
What the guard rail is for, and what it is not. It fires rarely, and for two distinct reasons
that were once recorded as one. On blind-corner cells it is not reached at all: the refractory
hold outranks its branch, and the starved samples the hold claims are exactly the stalest ones.
Where it is reached, which test binds depends on the population — on constructed cells the
margin does, with the shortfall present and large while the streak never reaches one; on real
cells the streak does, the shortfall holding for many consecutive rail evaluations while one
clearing evaluation resets it (walk, undo, return, retest and blind-corner samples neither count
nor reset it). The corollary worth keeping: between the veto's rate − 3·dev and the confirm'srate + VETO_MARGIN_MIN lies a band in which neither goal-metric layer can act, and the
machine spends real time there.
Deleting the rail is rejected by the shallow moat doses, where it is the only mechanism that
recovers the window and several audits arm over the run without rescuing it. Everywhere else
removal is free, and on the real cells where it fires it is worth about −0.01pp. So it is kept
because it is free and is the sole exit on a rowed adversarial family — which makes the case
conditional: if the moat rows were ever retired, the case for the rail goes with them, and §5'sagedown kill, which rests on disarming the rail, would need re-grounding. Two facts not to
re-derive: the streak is not consecutive in time (walk, undo, return and blind samples neither
increment nor reset it), and beginReturn's hold is cleared only incidentally, when the return's
own rate move trips isWorkloadShift — which suppresses density steering, not exploration, since a
due clock outranks the hold.
AUDIT_CRASH_PERSISTENCE's tolerance is one-shot by construction. Any non-crash ending,
including the tolerant retry's own budget expiry, retires the crash streak. A workload that runs
crash → fail → crash → fail for its whole trace therefore ratchets the ladder while learning
nothing; the streak legitimately measures consecutive crashes, and the one-shot form is
deliberate rather than an oversight.
5. The graveyard (do not re-explore without new insight)
2026-05 sweeps (~40 variants on real BLC, see large_cache_climber_findings memory): PID and all
descendants (integrator + no setpoint = corda catastrophe), feedback/ghost signals (break on loop),
Borkar, Smith predictor, RLS parabola, shadow/probe/MAB state-replacement (noise-driven switching),
EWMA smoothing alone, dead-zone smoothing (ap_dz, 28 cells, 6 big wins / 3 big losses — "each
fixes some, breaks others"), Gini/Indicator classifiers (skew does not predict the optimal
window), POSINIT. That campaign is the reason the density tier exists: it was a two-session
attempt to improve the reactive law at >512 and its verdict was "ship nothing". Its metric was
relative %, which the project later disowned, so the individual rows are weak evidence — the
dead-zone row in particular was re-tested properly in 2026-08 (below).
2026-07 density-era escapes (Opus session): kickoff-to-center (false-fires on w50 −8), EWMA-regret
(transient — re-traps after the EWMA catches up), annealing HR-walk (inconsistent), wide start
(fixes stress, destroys frequency traces −15), lowering/raising the floor alone (symptom patch).
2026-07 probe-machine ablations (this design's own dead ends, all measured — the tradeoff surface
is real, not tunable away):
- v6 small-entry probe + absolute exits: stray hits scale ~linearly with window size while
their density does not, so an absolute hit-count exit fires just short of the reuse band and
fail-adjudicates the probe (mixture_d025's escape died from a one-step stride change). - v7 density-competitive exits + travel budget (¼ max when the other region is healthy,
uncapped when dead): principled, kept every escape — but deep walks displace healthy-main
workloads (w50 doubled to −2.1) because a probe that starts in a low workload-phase never
triggers the crash-abort. - v8 give-up-jumps-ladder-to-max: no help; the first deep round is the cost.
- Verdict logic weaker than confirm-or-fail, and any threshold-only starvation test: see §3/§4.
- v9 slam-continue (walks continue past unconfirmed 4×bar hits under the budget): escaped
every deep synthetic (d050 +25, widepin +15, ratchet +3) but re-imported v7's deep-walk damage
on real thin-signal traces (w50 −2.2, S3 −1.5 vs the locked design) — the ablation that proved
the two-class tradeoff is rule-independent and motivated escalating commitment. - Early-confirmation exemption from the commitment gate: false mid-depth confirms reset the
ladder and neuter the escalation (d050_long 53.3 → 38.7 on the original binary; the exemption's
harm did not reproduce at N=3 on the shipped binary — era-scoped, the gate stays strict on the
measured tradeoff surface). The gate is strict. - Live-probation adjudication (the marginal verdict's unfrozen variant): measured as an ABSORBING
false-veto on the demoflood construct — a protected-saturating hot core demoted mid-walk earns
~11.8/slot in probation, so every walk is vetoed and the machine pins at the floor with zero
confirms (58.94 vs 67.17 frozen vs 74.10 incumbent-on-that-shape). The pre-walk freeze is
earned, not decorative; on every non-saturating family live ≡ frozen (mixture d010/d025/
d050_long identical). - Average-density up-probe verdict (the rejected simpler adjudication): vetoed real
reuse bands whenever the squeezed main stayed denser on average (F3 trickle −14..17, B1
bandtrap2 ×3 discard, widepin/aligned-phases pins) — replaced by the probation-marginal frozen
baseline; its one superiority (lucky diluted-average confirms on lowmix-class bistable low-HR
shapes) is the named, journaled trade.
2026-07 fresh-eyes study kills (all pre-registered, data in the
local hill-climber-study workspace ledger): per-sample goal-metric triumph
confirm (spoofed
by phase-rise traps; ratchet/troughwalk2); verdict-gated triumph (widepin +9.7 but two defended
traps regress and the pin persists); probe-time sample compression (halved-sample verdict noise
re-pins mixture_d040 −24); sketch-as-ghost repeat-miss lean (null — never engaged its cells);
confirmed-window warm start (null — widepin never first-confirms); boundary marginal-cost damping
(moved the give-backs +1.5 for the first time, but the admission-boundary comparison is degenerate
by construction — victims are always sketch-hotter — and the equilibrium-gated variant is inert);
and four honest-window-hits verdict forms (absolute / vs-main-average / vs-own-baseline /
vs-baseline-with-bar-floor), each trading a distinct family — the verdict-design tradeoff surface
in that study's report. The one survivor shipped: the rung-scaled walk stride (§4, the walk).
What each step is worth (the ablation prices)
Every step against the whole battery, 91 cells at seeds 1 and 2, beside the firing counts from a
ship run of each cell. Nothing is dead: corner reads 0 because that step was deleted, and every
other site fires. What each step buys where it acts, against what it spends elsewhere: the
retest 33:1, a repeat confirm's escalation 22:1, a park's first audit following its walk 20:1,
the frozen probation baseline 17:1, the audit layer 13.5:1, the return and retreat cover 9.3:1, a
reversed confirm's escalation 7.6:1, the walk's commitment depth 5.3:1, a blind corner's probe
4.9:1, the fresh-park shield 4.1:1, the refractory ladder 3.9:1, the rung-scaled stride 3.7:1.
Both candidates are closed. Restoring the deleted upper-corner probe (cornerprobe)
costs 9.61 across 18 rows against 4.32 across 5, and 27:1 against on that cell set
itself, so the deletion holds on the evidence that flagged it. The walk's commitment depth read
0.23:1 at N=8 then and 5.3:1 now.
The guard rail's veto is the one price that inverted, and the battery cannot decide it. On the
that cell set at the same seed it fell from 11:1 to 1.86:1; over the whole battery it reads
0.28:1, and at N=8 across the eight cells either rail arm moves it reads 0.33:1 with 10.95 of its
12.87pp cost on sidecliff alone (−1.34 to −1.42 on all eight seeds, while cp_w015 splits by
basin, +0.26 on five seeds against −0.26 to −0.37 on three). Every battery cell starts where the
product starts it, and a rail that returns the window to an anchor has little to defend from a 1%
start. Planted, it is worth +6.3 to +6.6pp on four of four seeds on mainsat at a 55% window
(32.42–32.68 against noveto's 25.90–26.65) and +0.15 to +0.52 at 70%. The step stays and themainsat plants are what holds it; without a planted cell the battery reads it deletable. Onsidecliff the rail is a net cost on every seed and the retest is what bounds it, sincenoretest there takes seed 2 from 59.25 to 51.63.
The retreat cover's widening is real and small. The commit that moved the retreat's
cover out of isParkTest, so it now runs without a held park. nowidecover (the cover scoped
back to a held park, the return half kept) is bit-identical on 66 of 68 rows and costs 0.07 and
0.08 on cp_w081's two seeds. So noreturncover's 44.12pp belongs to the held-park retreat cover
and the return half rather than to the widening, which is why the moat and hazefloor rows move
under that arm while the commit landing the return half read them bit-identical.
A detected regime change must not restart the audit schedule
The latency face of the top-corner residual: a discarding stand-down (isWorkloadShift and standDown with the
window on the anchor) restarts the audit schedule, so a detected regime change reschedules
exploration instead of leaving the rung and the wait the ended regime earned. Seven forms of the
one candidate, each read from trajectories on latebloom's alignment ladder at seed 1 and priced
on the rows that wanted the backoff seeded 1–8 (metronome, h4c1_attack, h4c1_reverse,whisper_mod_p6, whisper_mod_p12, whisper_mod_a12); the reset forms on the moat rows seeded
1–8; the two live forms on the unseeded battery with every mover re-adjudicated at seeds 1–8
with the arms rotated inside each seed, the floors and the corpus:
- rearm (the wait drops to
AUDIT_WAIT_FIRST; stillness and ladder untouched) and
rearmstill (the stillness run restarts as well): inert at the 64 alignment, since the first
re-armed audit goes down from the interior rest (the alternation state) and its failure at rung
64 re-doubles the wait to 128;rearmreadswhisper_mod_p6−5.35 andphases_d050−1.9
unseeded. - rearmreset (
auditClock.resetandaudit.reset, the cold schedule a resize gives) and
rearmboth (the retest's discard as well): the round-3hardresetkill re-entered through
the discard.whisper_mod_p6's period-6 swing is ±6–7pp, every swing at the floor anchor
discards it, the discard zeroes the stillness run, and with discards every three samples the
clock never reaches its four-sample wait: no audit arms in the whole run, the window sits at
the floor throughout, 55.58 against 66.01 on every seed (the F4 pin).whisper_mod_a12−2.9 at
seeds 1–8,mixmod_a010−12.2,widepin−5.7 andphases_d050@32k−5.3 unseeded,moat_h7800
−1.1 at seeds 1–8. The pinauditClock_rateSwings_doNotResetStillnessfails on this form. - rearmcold (every discard; the wait drops and the ladder resets, stillness kept): a
redistribution across the battery rather than a refinement.slowswap_step40.41 → 31.23 (−7 to
−13 on seven seeds),absolve_p845.95 → 42.50 (−5.7 on four),mixnoise_a1060.70 → 59.37 on
every seed (its bar is no drift below 60.4),h4c1_reversebelow LRU on three of eight seeds
(65.39 → 62.84),parkveil−5.2 on one seed; againstwhisper_mod_a1263.59 → 66.03 on every
seed (above LRU, and deterministic where the row was bimodal),crashnoise_a12+3.0 / +5.0 on
every seed,parkveil_min+5.0,mixmod_a010+2.1,whisper_mod_p6+0.83, the witness +8.75 on
five seeds. The moat rows bit-identical; floors and corpus within noise. Onh4c1_reversethe
low-phase pulse discards the unheld floor anchor, the wait drops with the stillness run intact,
so the audit arms on the discard sample with the pulse's dip as its base, reads the pulse's end
as its own reversal, and each such walk spends the rung: the 63% confirm the shipped machine
reaches at s46 comes at s101. - rearmheld (the cold schedule only where the discarded anchor was a held park, read before
the stand-down releases the hold) and rearmheld2 (the same withsettledRatekept, which
changes nothing on a12 and costs 0.5 on p6): the closest form. Battery 59 of 75 unmoved and
ten of the sixteen movers bit-identical at seeds 1–8 (the bimodal rows' draws); the moat rows
bit-identical at seeds 1–8;metronomeand the h4c1 rows bit-identical; floors within 0.2;
corpus within the spread (cp_w10045.77 → 45.23 against a 0.36 spread,cp_w098−0.30). It
earnswhisper_mod_p6+0.32,scarburst+0.54 and the witness +3.79 on five seeds, and it
pays on the F1 pair:whisper_mod_a1263.59 → 61.72, −2.2 on seven seeds against the row's
≥ 62.8 sentinel and 2.65 below LRU, andcrashnoise_a1261.69 → 60.13, the high basin lost on
four seeds. The post-shield swing discards the park, the restart arms an audit four samples
later from the park's position, and on the ±12pp swings those walks crash and spend the rung
where the kept wait spaces the audits out. - rearmheldcold (the held gate with the stillness kept):
whisper_mod_p666.01 → 63.31 on
every seed, since the post-shield discard arms the audit on the swing sample itself;a12
−0.39;metronomeandh4c1_reversebit-identical.
The latebloom earnings were the same in every reset form and are the candidate's measure. Where
the stand-down fires the gap to the ceiling falls 16.7 → 6.4 (phase 1 = 64), 15.4 → 5.6 (80),
11.3 → 4.9 (120), 9.8 → 4.9 (132) and 14.9 → 5.2 (64 with a 220-sample phase 2); the base is
bit-identical at every seed, since its arrival lands inside the retreat cover and no stand-down
fires; the witness rises on the five seeds whose arrival straddles the cover's end (−8pp on the
landing sample, covered, and −6pp on the next, not); the 48 alignment pays 3.5 → 6.9, an audit
fired from a still-good unheld position four samples after the undo-landing discard walking onto
the flat shelf and failing at budget; and the residual in every form is the alternation's side
choice from the interior rest point, the first re-armed audit spending sixteen samples walking
down to the floor before the next one goes up. The clock alone cannot carry the deep rung, and
the ladder reset is load-bearing: with the rung kept, the first re-armed audit's failure at rung
64 re-doubles the wait.
No form gates clean because the discarding stand-down is the machine's one regime-change
detector and it fires on every crash-scale move at the anchor, which the periodic families
produce every few samples (the track-planted anchor at every swing, the park at the first
post-shield swing) and the pulse family at every dip. The shipped machine pays those discards
with the anchor alone and keeps the schedule. Letting them touch the schedule loses whichever row
the form's shape exposes: the stillness restart starves the clock on the swings, the kept
stillness arms on the dip, and the held gate trades one whisper_mod row for the other. The
distinction a landable form needs, a level change that persists against a swing that returns, is
the signal-classification wall (§5, Ageing an off-anchor claim); a restart deferred until the rate has stayed a margin
below the discarded claim through a settle is a new state inside the confidence-gate graveyard
and was not built. The seven arms stay in the harness under one flag table (rearm*), so the
square can be re-priced after an alternation change.
A regime change can also miss the detector. standDown discards only with the window on the
anchor, so a step that lands while an audit's undo is still carrying the window home keeps the
stale claim; its veto later drags the window back, and the next audit waits out the ended regime's
clock. slowswap_step shows the size: moving its swap 16 samples later (slowswap.py --p1 166)
puts it inside such an undo, and the shipped climber reads 33.3 against 40.3 at the committed
timing, holding near a 25% window with a 1% hit rate through a 64-sample wait while the new regime
wants 80%. Discarding on an off-anchor step meets the same wall, since the controller's own moves
also step the rate.
Guarding the probe's arming, and re-reading its verdict, on a regime-change sample
Four guards on the starvation probe's arming, built for arc_DS1's trough-armed confirms at a
1M maximum (§3's thin-signal floors; the v7 shape above), each priced on the unseeded battery,
the floors and the corpus, with the movers re-adjudicated at seeds 1–8 and the arms rotated
inside each seed:
- shiftarm (a blind corner on a sample whose rate moved by a restart threshold from the last
closed sample holds one sample before arming): DS1 11.99 → 13.74 at 1,000,000 and 13.45 →
14.77 at 1,051,635, floors and corpus within noise, and the phase-alternation families lose
their escape, since the flip sample is the blind sample and a hold on it arms nothing when the
next sample is sighted:phases_d050−10.6,widepin−9.4,phases_d050@32k−4.4,shallowmoat−2.7 (unseeded N=8/8/5/2), againstmixture_d050+5.7,blindlock_blind+5.4
andmetronome+2.3, where a shift-armed probe was failing and imposing its refractory. - shiftarm0 (shiftarm, and the first sample holds as well): the above plus
demoflood
−10.6,absolve−5.2 andslowswap_ramp−2.9. The first-sample probe is the escape on those
rows, so a first-sample hold on its own (coldhold) was not run. - shiftdead (the hold only where the sample is dead, both regions starved, and a shift: the
v7 walk armed in a trough and nothing wider): DS1 +1.73 at 1,000,000 and +0.42 at 1,051,635,
since two of the four arms there are on samples with main at 8k–146k hits against a bar near
4k (the wedge round's "low samples straddle the starvation bar"); 14 of 16 re-adjudicated
cells bit-identical at every seed,widepin55.48 → 47.80 (five of eight seeds at −8.4 to
−13.7; its bar is no per-seed drift) andphases_d050−0.69 on four seeds.
The rows that pay are the ones whose escape is a probe armed on exactly the sample the guard
holds.
The verdict side was then exhausted on Ben's ask, three forms on the same cells and battery:
- rebase (a starvation walk whose sample rises by a restart threshold re-freezes its base,
rate and probation density, on that sample once and walks on; rebasepos returns the confirm
to the rebase sample's window): DS1 13.83 / 14.62 at the two sizes, 27.44 at 2M (an extra
stride), andphases_d050@32k−13.7,phases_d050−12.3,widepin−5.9,norank_rep_r6
−3.2. Re-freezing the probation baseline on a live sample is thenofreezefailure for the
escape: the walk's own demotions enrich live probation into a veto. - deadbase (a walk armed on a dead sample is adjudicated against the live probation density):
DS1 14.07 / 13.59 (alignment-limited asshiftdead),slowswap_step−6.7,widepin−5.2,phases_d050@32k−3.5. - deferrise (a crash-scale rise defers the verdict one sample, holding the position, with only
the crash reference moved and the verdict baseline kept frozen at the arm): DS1 13.91 / 15.12
(above the hit-rate law's 14.91 at the gate's size), 2M −0.09, S3 +0.13, andphases_d050@32k
−13.2,widepin−9.4,phases_d050−7.4.
Every form that refuses or defers a verdict on a regime-change sample loses the phase-alternation
escapes, since on those rows the flip sample is the only sample the verdict can be read in: the
regime alternates at the sample cadence as DS1's does, and there the larger window is right. At
the decision the two are one signal. DS1's cost at this size stays the wedge round's accepted
price;wedgeshift(above) remains the recorded guard for the shape, still not
landed; the untried lever is the sample period itself (dens10xreads 14.34 at 1M by taking four
samples instead of ten, and on S3 +0.2 / +1.0 / −0.6 / −0.3 at 100k–700k), a cadence question
rather than a rule for this shape.
Whole alternative controllers
- Marginal-loss controller: locally right is not globally sufficient. Exact loser-refault
attribution was locally informative: all seven screened deltas were non-negative (0 to
+1.80pp). The controller built around it still failed its final-regret gate. Moving a fixed 1%
of capacity every 2C requests left the ten-cell nearest-rank p90 regret at least 2.83pp against a
1.5pp bar. P3 states the limitation cleanly: hit rate falls while moving from the 1% start toward
roughly 20%, then rises to a remote 80% basin. The local signal correctly points back toward the
floor and therefore cannot discover that basin. Adjacent-direction accuracy and global search
regret are separate gates. Keep the signal as a possible diagnostic or local trim input, not as
evidence for a replacement climber. - Replicated MiniSim: useful information, rejected realization. The first prototype was
invalid: ordinary static W-TinyLFU arms recomputed probation at each window size, while the live
host keeps probation fixed and transfers capacity between window and protected. The repaired
experiment constructed reachable integer segment triples, clamped and deduplicated targets
before scaling, followed capped moves to the exact approved coordinate, and supplied the same
request-indexed admission variate to every arm. At the co-minimal 128-entry, three-cohort setting,
its six-cell median and p90 static-oracle regret were 0.197pp and 0.305pp: remote counterfactual
ranking did contain useful information. The 27-policy realization then failed every production
cost bar: 103,816 bytes retained when empty, 314,784 bytes steady, 24 bytes per sampled hit, a
median 1.96 bytes/request on the natural 95/5 mix, and a 37.52% median hot-hit throughput
regression. This rejects that realization, not the information source.
The useful correctness subset was later extracted to main: registered MiniSim now preserves the
live fixed-probation geometry, removes full and scaled aliases, and computes each integral move
from the host's actual window coordinate. The expensive replicated controller remains rejected.
Equal RNG seeds still do not give its arms request-indexed common draws, so MiniSim should not be
described as a fully paired counterfactual oracle. - Learners over sampled miniatures: better on quality, rejected on cost. A learner over the
registered MiniSim miniatures (fixed-probation copies of about 100 entries fed a 1/R key sample)
that keeps a discounted miss total per arm over a 64·maximum horizon and moves the host window to
the leader gains a median of about 3.7pp overproduct.Caffeineon the gate battery, with no row
worse than about 1pp, and 0.5 to 1.1pp on eighteen real density-tier cells (simulator host,
unseeded, one to three draws per cell). Five arms at 1/20/40/60/80% keep the gain, so the prize is
reaching the right basin rather than resolution. Its real-trace wins are the recorded give-backs:
the average law's rest point on frequency-favorable traces (thecp_w097family 4 to 6pp,P8
andS31 to 2pp) andcp_w050@123038's start path. Its own failure is miniature fidelity: just
afterDS1@2Mfills, the miniatures rank a 76% window above 1% where the full cache scores 56.8
against 61.9, costing 1 to 2pp there and at 4M. Three variants are dead. Hedge at a static-regret
learning rate (H-MC, Ben Mazziane and Zou 2026) cannot follow a regime change: −5.9 on the corda
stress at 512, −1.4 and −3.6 onslowswap. Its maximal coupling of consecutive picks buys fewer
moves and no hit rate over a deterministic leader. The registered per-epoch argmin chases
alternation, losingphases_d050,widepinandrep_r6by 6 to 12pp to either learner with
memory. Rejected as a product mechanism on costs that grow with the arm count or the maximum:
per-entry nodes and an eviction loop for each miniature (only the sketch and a key table can be
shared across arms), miniatures sized in proportion to large maxima, and a grid to choose. Kept
as/audit-regret's ghost, the simulator'sminisim-leader, because it is an online policy
rather than a hindsight one: it can exceed every fixed window on a phased trace (39.3 against
33.5 on the corda stress at 512), and its copies' misses at each sample say which window that
sample rewarded, the per-phase reference against which the skill reads the climber's costly
episodes. As a gap-finder it adds little: its gap to the climber ranks cells as the static gap
does (rank correlation 0.99 over the gate rows, 0.94 over real density-tier cells), and its own
window, flat on the traps and dithering among near-equal windows on real traces, localizes
nothing. Its configuration is measured. With five arms and MiniSim's ~100-entry miniatures it missed narrow optima
(crestpast62.6 against a 64.7 ceiling), and at finer grids the miniatures mis-ranked near
neighbors (moat_h780048.3, where five arms read 49.5); miniature size was the larger lever.
Seventeen arms at 5% steps with full-size copies up to 20,000 entries read 64.6, 56.8 and 50.8
oncrestpast,metronomeandmoat_h7800(ceilings 64.7, 55.5 and 49.7), sit a median 0.1pp
above five arms across eighty cells with one row lower by more than half a point (widepin
−0.8), and forty-one arms add nothing (a mean 55.60 against 55.63 on the twenty cells where the
configurations differed). It lands below the climber onDS1at 2M and 4M at every size tried. - Bounded reuse-survival estimator. A smaller primitive estimator was a credible attempt to
retain the remote-ranking information without full policies. Its registered realization missed
both information bars: 0.727pp median winner regret against 0.5pp, and 2.849pp p90 against
1.5pp. Retire that realization as controller authority. The broader reuse-survival idea was not
disproved by one resource setting and seed family. - Compact Shadow prototype. Its controller semantics and bounded layout checks passed, and the
observer allocated nothing in its steady interval. The first pair of the terminal four-cell cost
gate then measured 32 bytes per candidate command against 16 bytes for the baseline, so the
frozen stop rule ended the study before quality was scored. Compact state is possible; an
affordable observation seam and a quality win remain unproved. There is no superiority result. - Archive boundary. The trace-metadata prototype, compact primitives, static oracles, runners,
manifests, and experiment evidence plumbing have no upstream consumer or shared owner. Keep them
archived until one exists. Whole-file transplants and a publicSimulatorAdmissionRandomAPI
are rejected. A future consumer needing exact randomized pairing must first show that both the
draw count and request-index digest match.
Cheaper forms of the veto's return re-test
- A hold before the retreat commits. A completed shortfall streak stands the window still for
two samples and re-samples, cancelling the retreat if the rate recovers to within the veto
margin. Endpoint +1.16 against the retest alone's +1.39, three negative seeds of sixteen against
none,norank_flood_j100−0.08 andshieldtrap_s11−0.01 where the retest alone is
bit-identical, andcp_w081−0.42 on the corpus where the retest alone is +0.07. The mechanism
is self-defeating: spreading the arrival drop across the hold's own samples is what puts it
under the crash-scale threshold, so the hold manufactures the masking the retest then repairs.
The cancel branch never fired on any cell. Do not re-propose a pre-retreat hold without a cell
where a veto is shown to fire on noise.
Other ways to judge an audit's verdict against its walk
bestgap(the verdict pulls back only past one stride of separation, the arrival transient's
own width):cp_w097+0.73 againstdemoflood−3.59 andmixture_d050−0.33.demoflood's
real crest andcp_w097's noise spike are both one stride behind the walk's end, so the
pull-back's distance does not separate them.bestrough(the pull-back must beat the largest sample-to-sample move of the walk itself):
restored the then-baselinecp_w097value recorded by the07c6e370ccrestpast study
(47.61 under its original unseeded simulator protocol; not comparable to the fresh seeded N=8
mean) and costsdemoflood−3.59 again plushazefloor−0.73. Two independent statistics refusing a
real crest in order to refuse a spike is the shape of item 1's finding on this layer: the signal
cannot tell a crest from a peak.monoland(the margin plus a monotone decline from the best to the walk's end): inert,
bit-identical to the shipped form on every decisive cell. Where the margin has decided, the shape
test has nothing left to refuse.bestflip(a park short of the walk's end points the next audit the other way, since the
ground beyond is now known worse):hazefloor−3.92. The follow rule's value is the direction the
confirm earned, not the unexplored side.landclaim/refclaim(a covered landing keeps the claim but releases the hold, so density
may still improve on it):moat_h4000−2.29, a re-derivation of the recordedarrivekill —
without the discard'sRates.resetthe rail's margin stays at its 1pp floor andtrackre-plants
the anchor down density's slide.retreatref(the retreat's samples judged against the rate the probe left, thefreshref
rule applied to the stand-down rather than to the confirm): weaker than covering the landing
outright (crestpast−0.09, the plant-at-40 cell −0.45), because the landing's recovery is itself
an arrival transient that takes more than one sample, so a level test against the pre-walk rate
still fires while a first-difference test no longer does.
Other ways to un-stale an away-anchor claim
- Discarding the claim on a crash-scale swing that lands with the window still (
stilldiscard,still2,still3,quiet): fixes the family exactly as the landed reference change does, and
costs the rail's control rows because a still swing is not necessarily the workload's. The one-
boundary form mistakes a retreat's echo (moat_h4000: the undo arrives, the collapse lands one
sample later inside the band, −2.3 / −3.4); two or three boundaries excuse the echo but not the
terrain's erosion at a still window (moat_h3000: fourteen still samples, the window's hits fall
5,662 → 112 with main's flat, then a −7pp break: −0.7 to −1.9 on eight of eight). The claim the
swing discards is the machine's memory of the prize the rail then recovers (veto to 0.62 kept,
0.46 discarded). Region composition would separate the cases seen (a shift moves main's hits, the
terrain's collapse only the window's) and was not built: a second threshold on a heuristic. - Re-seeding the goal metric on a still swing while keeping the claim (
resetstill,freshreset): the re-seeded deviation prices the shortfall against the kept claim as real, the
rail vetoes into the dead anchor within four samples and the audit that then arms from it confirms
a mediocre position (ghostclaim_p3041.2 against the reference change's 49.2). The deviation
spike a shift leaves is what holds the rail off while the audit walks first.
Variants of the repeat-confirm memory
- The repeat memory cleared on the anchor's discard: the confirm sample is the lure's on-step and,
where the walk lands at the previous anchor, that step discards the anchor on the sample the confirm
is judged; the chain breaks (absolve_p8seed 1 41.4 against 46.4 for the fail/crash-reset form). - The park's first audit following the confirmed walk unguarded (
momentum):absolve+16.7,veilmoat+3.7,shallowmoatbasin B +2.7,flood_j100+4.1 (N=8),moat_h3000+1.6,moat_h4000
+0.8,crashnoise_a12+0.5,shieldtrap_s13+0.6 againstmoat_h5000−1.75 (four seeds at −3.4: the
far slope's gentle decline takes five strides to trip the 5pp bar, the next audit is the tolerant retry
and its undo evicts the band),whisper_mod_p6−1.76 (its sentinel),climbtrend_up−0.9 (its
sentinel: trend misconfirms swept wall to wall),saw_p40−0.5. The guard on the smoothed rate is what
ships. - The guard on the confirm sample's raw rate against the arm sample's: fragile to the lure's phase
(absolve_p12seed 1 falls back into the trap, +0.6 against +17.6) and keepsmoat_h5000−3.4 andsaw_p40−1.4; the smoothed reference is robust to the phase and fires on the trend.
Releasing a park that the workload has starved
unpark(a parked, non-blind window starved for a sample discards the anchor and hands the
sample to density;unparkbothalso on a starved main): fires at three sites in 642 audit
confirms and loses one of them big (phases_d050@32kseed 8 −5.2 againstrep_r6seed 4
+3.8). The park on a starved position is priced by rarity, and the census is the instrument to
re-run before proposing any rule about it.marggateandmargrestonflood_j100: both are the average law there. The gate's
"probation richer than main's average" test is satisfied by the SLRU geometry alone whenever a
rotation exceeds protected (every promoted key is demoted before its reuse and protected earns
nothing), 47 of 48 steering samples, and the rest band is entered only at 57–76% windows. The
marginal signal itself is right on the cell (tail hits 0 at every window; ungatedmarghalf
63.19 against 51.98) and dead by the frontier above.arrivealone (an undo's or a return's landing sample is not judged for a shift): moat_h4000
−2.69, h5000 −0.50, h3000 −0.36 on every seed. Without the discard'sRates.resetthe rail's
margin stays at its 1pp floor andtrackre-plants the anchor down density's slide into the
valley, so the rail's moat win turns out to rest on the reset widening its margin. (The
undo-landing half of this arm is what the verdict covers; the return half, re-expressed through
the retest state once the retest existed, was implemented asisReturnTest, and on release HOLD by the repeated-real-trace recheck.)auditshieldalone (a shift during an audit walk armed from a park does not stand the park
down): bit-identical to ship everywhere; the undo's arrival at the anchor discards what the crash
spared.arrive+auditshieldas an unconditional pair: moat +6.5 in total andflood
+2.2 againstdemoflood−1.88 on every seed (its bar) andwhisper_mod_a12−1.83, the park
held where ship's release let density drift to a better position. The park-retention trade with
its mechanism; a scoped form on the top-corner residual is where it would be worth something.pricedshift(the stand-down's shift trigger priced as a starvation probe's crash bar,
clamp(3·dev, [5pp, 15pp])): crashnoise_a12 +1.71 and deterministic,rep_r6seed 4 +30.9, and
cp_w081 −0.39 on 4 of 4 seeds, arc_P8 −0.19, arc_ConCat −0.13, moat_h7800 −0.94 (a basin
collapse). The fixed trigger's churn on noisy cells is inert on the corpus and protective on
cp_w081, where a persisting anchor lets the rail veto on slow weather. A trade; do not re-derive
the level, and do not read the C3 churn as a defect without this cell.nocornerwas the diagnostic that priced the upper corner's probe here (+2.3 rep_r6, +3.8
balloonflip against −0.2 on cp_w015 and arc_ConCat, read then as "the probe stays"). The probe
was deleted and the arm with it;cornerproberestores it, and the
battery reads the restoration 2.2:1 against.deferreward(the wedge pricing's next-sample half): a redistribution ofrep_r6's seeds
(+3.0 mean, one seed −20.9) and inert or slightly negative elsewhere (phases_d050−0.49); the
ladder's doubling is coarse against a 91-sample trace, so any faster escalation wins the seeds
where the deeper probe finds the band and loses the one where it hits a thin burst. Not landed;
the arm is in the workspace if the sentinel's mean is ever worth that.
Other prices for a confirm the density arm reverses
The round priced a starvation confirm that the density arm reverses in the same sample (a
"wedge"). What shipped is the escalation without a wait; the rest is dead:
- wedgehold (the wedge keeps the rung, neither reward nor escalation) and wedgefail (the
wedge is a failed experiment: undo, escalate, refractory = rung): both hold the floor long enough
for the calibration audit to arm and misconfirm on the warmup trend, soshallowmoat's wedge
seeds read 27.5 / 27.3 against ship's 28.5, and neither reaches the prize inside 64 samples. - wedgewalk (the wedge does not end the walk): the v9 slam-continue family (§5 above), kept as
the diagnostic upper bound (33.5 on the wedge seeds) and not promotable. - wedgeflat (only a wedge on flat terrain escalates; one across a restart-magnitude change
keeps the rung):trickle_s7−6.7 (four seeds at −14) because the kept rung makes the next
fail's refractory 32 samples instead of the reward's 2. - wedgedead (a walk armed on a dead sample is first-round):
arc_DS1@1051635−1.7; its low
samples straddle the starvation bar (2,662 main hits against a bar of 4,108 on one, 10,221 on
the next), so the second is a rung-64 walk to an 80% window. - wedgelow (a walk armed a restart threshold below
rates.smoothedis first-round): never
fires on the alternating trace, because every sample is a shift andrates.resetre-seeds the
smoothed rate to the arm sample itself. - wedgeshift (a walk armed on the sample the rate fell into by a restart threshold is
first-round; the guard the shipped rule can carry): measured and not landed. It restores DS1 anddeadphasebit-for-bit and removesphases_d050's two seed flips, is bit-identical on every
non-wedge cell run (arc_P8, arc_S3, cp_w015, cp_w050, mixture_d050, straywall2, slowswap, moat),
and gives backstrad_p8@4097+1.9 → +0.5,widepin+1.7 → −0.8 andphases_d050@32k+4.8 →
+2.7. Its evidence for the floors is one cell (DS1 is the only floor cell with any starvation
confirm), so the pass is by construction; the instrument that would price it is the unspent
stillness holdout. Add it if trough-armed deep walks show up at scale.
The 1% initial window is not worth moving
PERCENT_MAIN has never been re-asked since it was chosen. The hostile-window study above gave it
an instrument, so it was swept properly: 77 cells classified by their measured static optimum
(43 frequency-favorable at ≤5%, 14 middling, 20 recency-favorable at ≥40% — the axis the hostile
study could not see, since its cells were screened for a small optimum), then 37 of them swept
at starts of 1/2/5/10/20%, plus a 30% arm on the 23 density cells.
The bar was fixed before measuring: corpus mean ≥ +0.50pp, no stratum mean below zero, no
workload family losing more than 1.0pp, holding at each tier separately. The best arm returns
+0.049pp, a tenth of the bar, and no arm satisfies the stratum clause. Applying this study's own
readability floor (≥15 density samples) the whole table collapses further: every arm's corpus mean
lands within ±0.21pp of zero, the best being +0.018 at a 20% start. The mid stratum's apparent
+0.25..+0.87 is entirely corda@2k and corda@4k at 3.3 and 6.6 samples, which price the
start rather than the controller; with the floor applied that stratum is negative at every arm.
The mechanism is why, and it is the part worth keeping. The climber absorbs the plant
asymmetrically: on recency workloads it captures 96% of what a larger start could statically
buy (frozen +2.68 against a delivered +0.11 at a 10% start), because it was walking to a 20–50%
window anyway and the plant only shortens a journey already underway. That assumes the climber
walks; on the LLM prefix-cache cells (§3, terrain classes) it walks too slowly for one pass, where a
plant does pay (one pass of To-C@185364 gains +8.7 from a 10% start and +17.5 from 50%), though with
fresh-key replays it reaches the large window within two to four passes anyway. On
frequency workloads it passes 90% of the harm through (frozen −0.80, delivered −0.72), since there the plant is pure
displacement to be walked back at the descent rate of 4–6% of the maximum per sample. A larger default therefore buys almost
nothing where it would help and costs nearly full price where it hurts. On two cells it lands in a
worse attractor than 1% reaches at all: wiki_1191a@64k frozen −0.12 → delivered −0.99 on
3 of 3 seeds, arc_P13@64k frozen −1.59 → −3.41.
The storage families are the case against moving it: the ARC set is 16 of 18 frequency-favorable
with an optimum at a 1–2% window, losing monotonically as the start grows (frozen −0.15 / −0.44 /
−1.18 at 5 / 10 / 20%, worst −5.41). A 5–10% default is a straight tax on exactly the large,
frequency-signalled caches the 1% choice was made for.
cp_w050@123038's basin (§3, Path-dependent rest points) does not argue for a larger default: at N=8 the 2/5/10/20% arms
read +0.02 / −0.01 / +0.04 / −0.21 and all converge to the same ~20.6% attractor, and only the 30%
arm moves (+4.72, and one seed of eight already falls into the bad high basin from a 20% plant).
It is a threshold between 20% and 30% that a 5–10% default captures none of, and it remains a
question about the audit layer rather than about the default.
Not decision-bearing but recorded: strata are near-collinear with families in this corpus (ARC 16
of 18 frequency, cloud-physics@16k 12 of 14 mid-or-recency), broken only by scarab_recs,wiki_1191a, msr_prxy_0/hm_0 and the lirs reactive cells; cp_w097/098/100/101@16384 are
near-duplicates and count as one workload; and cp_w050@2048 has its whole static curve below
LRU (ceiling 13.65 against LRU 14.52). No holdout was spent, and none is owed, since nothing was
fitted. Two rest-point give-backs above the documented 0.5–2.5pp band turned up in passing and
belong to the marginal-steering thread (§5) rather than here: arc_P1@64k settles at a 30% window against a 1% optimum
for 3.25pp (45.63 against a 48.88 frozen ceiling). cp_w097@16k is the other face, but it does
not settle: its 135-decision endpoint is near 47% against a broad 5–15% optimum, and continuous
270- and 541-decision runs stay near 47% while cycling through seed-dependent walks and parks.
An LRU estimate as an audit trigger
A sampled estimate of what an LRU of the same maximum would hit (SHARDS-sampled keys in a
fingerprint-and-stamp table of about 25 KB, read through AET with the SHARDS-adj correction, within
about 1pp of exact LRU) can schedule an up-audit when LRU out-hits the cache by more than 3pp for
four samples. Under plain multi-pass replays it brought every LLM prefix row within 2.6pp of LRU at
steady state and lifted the battery by +1.6 with no bar failing, but the steady-state win was the
replays' frequency memory (§6): with fresh keys per pass the shipped climber converges within two to
four passes on its own, and the trigger adds 0.0 to +0.8. It also cost 16% of EvictionBenchmark
throughput at 5,000 entries and 9.5% at 10,000, where 2,048 sampled keys cover 41% of a 5,000-entry
cache's requests; capping sampling at 1/16 of keys cut that to 2.7–3.7%, the rest being per-request
work. Early in a run the cold frequency sketch lets LRU lead on frequency-favorable traces too, so a
lead-triggered audit ahead of the climber's own first audit confirms premature windows. What
remains is +1.6 on synthetic traps, which does not pay for the state and the throughput. Price a
per-request sketch at the smallest maximum it serves before validating its hit rates.
Accelerating warm-up: jumps, momentum and shorter samples
On a recency-favorable trace the climber reaches its large window, but slowly. On the LLM
prefix-cache cells (§3, terrain classes) one pass of a two-hour trace is 8.5 to 25 samples at the
larger sizes, and the climber spends 9 samples (To-C@185364) to 74 (Thinking@32768, about three
passes) at the floor before steering climbs: a grown window reads sparse while main holds the
captured hot set, and probes and audits from the floor fail on phase swings. The rules that shorten
that phase buy +1.6 to +2.8pp on those first passes, and each costs something on every seed: a
frequency-heavy real trace 0.7–2.4pp or, once gated so that no real trace triggers it, the
battery's traps. None passes the bar the start-window ruling above set:
- A geometric up-audit from the floor (stride doubling while the hit rate holds), with dip
tolerance and arming ahead of the calibration wait: +2.8 on the prefix cells, but To-B −1.2 and
ARC S1 −2.7, S2 −1.2, S3 −0.9. Gating it on an early sample in which the window out-earned main
leaves the controls bit-identical but not ARC, since a cold cache makes every window look dense in
its first sample. Judging it against its base plus the hit rate's prior rise, with arming held two
samples, keeps +2.4 (To-C@185364 +7.4) and still costs P8 −0.9, S3 −0.8, S1 −0.7. - The same jump behind a fill-drop gate, which allows it only when the hit rate falls once the
cache is full (the second and third samples averaging under 70% of the first): every corpus cell
(seeds 1–2) and control is bit-identical and the prefix cells gain +1.6, but the battery fails
slowswap's step and ramp (−11.7, −9.3) and mixture_d050_long (−1.3), each on every seed, while
absolve and absolve_p8 gain +9.2 and straywall2@16k +4.0, for a battery mean of +0.4. On
mixture_d050_long the doubling strides land at 80% and park at 69%, where the shipped walk parks
at 56%. The early audit also moves every later one, so on slowswap an audit crashes just before
the swap; the swap's rate step lands during its undo, away from the anchor, wherestandDown
ignores it, and the stale anchor vetoes the climb. That trap is timing-sensitive for the shipped
climber too (33.3–41.8 asslowswap.py --p1moves the swap ±24 samples, the row's committed
timing near its best), but averaged over those timings the jump still costs 2.1. Handing the
landing to density steering instead of a park does not escape this: at To-C@185364's 80% landing
density reads the window sparse (log ratio −0.8, main still holding the captured hot set) and
would give back the gain, while at mixture's 69% overshoot it reads −0.5 and would correct it,
so the landing carries no signal that tells the two apart. - Momentum, consecutive same-direction density steps growing ×1.5: +0.3, too small.
- Samples of one maximum instead of four until the first audit: +2.2 (Thinking@65536 +5.1), but
DS1@1M −2.4 and cp_w097 −1.2. DS1's first audit never runs, its 0–68% per-sample swings drive
density-judged probes to a 40–62% window, and capping the short samples at 8 or 16 still costs
DS1 −1.6 or −1.2.
Two things recur. The first samples after the half-full gate are cold: the hit rate rises with the
warming cache (ARC S1 climbs 1.1 → 6.9 over the first ten of its 15 samples) and the empty frequency
sketch makes the window look dense, so a verdict taken then is biased. And an early move persists
through parks, backoffs and the audit schedule (P8's jump crashes cheaply at 20%, yet a later audit
confirms larger windows on every seed). So the ungated rules help the traces whose right move is up
anyway and charge the ones whose right move is to stay; the fill-drop gate removes the first problem
and still fails on the second. Warm-up stays as it is; a rule that could pass needs an early move
that neither parks its landing nor shifts the later audits.
Neither main-space knob is worth adapting
The main space has two elements the climber never touches: the 80/20 protected/probation split
(PERCENT_MAIN_PROTECTED) and the 1-hit promotion rule (reorderProbation). Merlin adapts the
analogous quantities, so both were swept as a second dial: P in {0.20, 0.50, 0.65, 0.80, 0.90,
0.95} and a promotion gate requiring the sketch's estimate to reach T in {1, 2, 3, 4} — the
zero-metadata analogue of Merlin's guard_freq, needing no new per-entry field and no scan.
Object-count, the 288-cell Merlin matrix minus the two multi-GB traces, one build, the shipped
arm re-measured rather than inherited.
- There is no better fixed constant, in either knob. Every alternative loses on the corpus
mean: p90 −0.051, t2 −0.031, p65 −0.074, p50 −0.155, t3 −0.140, p95 −0.191, p20 −0.249,
t4 −0.373. The shipped 0.80 / 1-hit is the optimum, not merely a defensible default. This is
the opposite of Merlin's own region dials, where the ghost budget was worth +1.49 mean and the
filter ratio +0.83 (merlin_region_sizing_headroom) — those were bad defaults; this is not. - The hindsight prize is inside the instrument. Best-of-9-arms per cell is +0.326 mean, but
product.Caffeineis not bit-deterministic and max-of-9 is biased upward even when the arms are
identical: re-running the shipped arm nine times on a representative 26-cell subsample buys
+0.116 from noise alone. Net +0.21 pp for a perfect per-workload oracle. Any real controller
captures a fraction of an oracle, and would be paying for it with a second extremum seeker. - The response is real but its argmax is not learnable. 16/26 cells show a P spread exceeding
twice the same-arm noise range, so the surface is not noise. But the winner is scattered almost
uniformly (p95 57, p90 54, base 50, p20 43, p50 38, p65 34 of 276), 46/46 traces change their
preferred P across the size ladder, and the mean response is flat to ±0.1 pp at every size rung
with the peak at or adjacent to 0.80. There is no stable per-workload optimum to seek. - The within-trace pass, which is the one that could have overturned this, closes the same way.
A per-cell oracle prices only a per-workload controller, so each arm was re-run on three disjoint
thirds (trace.skip/.limit, binding confirmed by segments that genuinely differ). On
selected high-spread cells the per-segment prize looks large — +1.075 mean, up to +4.11 — and
the per-segment noise floor is small (0.16–0.39), so those gains are real rather than noise. On
an unbiased 14-cell sample it collapses to +0.225 gross, ≈0 net. The difference is cell
selection, not workload regime: splitting by adaptations available in the segment does not
separate it (<15: +0.184, ≥15: +0.256), so the tempting "it is climber non-convergence" reading
is not supported and should not be repeated. - The downside dominates the prize by an order of magnitude, and it is a cliff. p95 costs
−1.76 at the 5th percentile and −6.16 worst; t4 costs −25.72 onbackf@300. A controller
would be risking multiples of what a perfect one could win. - The promotion gate is inert where theory says it should be. t2 changes nothing at all
(|Δ| < 0.01 pp) on 55/220 cells: an entry that won the TinyLFU admission contest already carries
frequency ≥ 2, so the first threshold step is a no-op. The gate only bites by starting to deny
promotions that should happen, which is the −25.72 tail.
Two properties of the structure are worth keeping in mind for anyone who reopens this. BecauseincreaseWindow takes its quota from mainProtectedMaximum and caps there, window + protected is
conserved and probation's capacity is fixed for the cache's lifetime at (1−P)·0.99·max
≈ 19.8%; the window's reachable range is [2%, ~80.2%] and it can never grow into probation. That
also means the low-P arms conflate probation size with window ceiling (p20 caps the window near
20.6%), so their losses are a lower bound on p20's true merit — but p90/p95 carry no such handicap
and fail on the mean anyway, so the verdict does not rest on it.
A constructed two-reference band between the window's reach and LRU's (s_probcapgeo) costs every
reachable split 32pp against LRU, and a larger probation at the same window recovers none of it
(the filter refuses once-seen candidates); only a window of 89–92% catches it.
Not covered: ConCat and MergeP (12 of 288 cells, ~9 GB of trace) were not run.
Companion result: the SLRU main space itself is still earning its keep against a plain LRU
main. The 2015 commit that introduced it ("Use Segmented LRU for the main space cache") claimed
0–2% in many workloads, especially small caches, and the question is whether that survived the
sketch fixes since. Asked by setting the promotion threshold to 16: the sketch saturates at 15 andreorderProbation is the only path into protected, so nothing is ever promoted, protected stays
permanently empty, and main degenerates to one LRU queue with the climber's window boundary
intact. (PERCENT_MAIN_PROTECTED = 0 is not the way to ask this — it zeroesmainProtectedMaximum, which increaseWindow early-returns on, so it measures a crippled climber
rather than plain LRU.)
Over the same 276 cells, plain LRU main is worth −93.1 pp net, mean −0.337, median −0.227.
SLRU wins by >1 pp on 40 cells and >2 pp on 8; LRU wins by >1 pp on 9 and >2 pp on 3; 227 cells
tie within 1 pp. 38 of 46 traces prefer SLRU. Confirmed at N=3 on the 14 headline cells with
0 sign flips (σ ≤ 0.31 on all but P1@69344, whose N=1 +1.48 collapses to +0.07 — a noise
cell, not a result). The 2015 claim's size dependence also survives: the advantage concentrates on
the smaller rungs (rung 1 −0.596, rung 2 −0.601) and vanishes at rung 4 (−0.002). So: keep SLRU,
but it is a modest, concentrated effect — it is inert on 82% of cells and worth 1–3 pp on the
rest. The traces that genuinely prefer a plain LRU main are a coherent set worth remembering:fiu_ikki +0.851, fiu_webmail +0.548, backf +0.326, loop +0.305, MergeS +0.259,fiu_homes +0.186, ps +0.136 (and corda_large +0.002, a tie). They are the same cells that
wanted a smaller protected region in the P sweep, so the preference is consistent across both
knobs — it just isn't worth a controller, per the headroom result above.
The promotion curve is non-monotone, and that is the interesting part. Mean vs the shipped
promote-on-first-hit: @2 −0.031, @3 −0.140, @4 −0.373, @6 −0.640, never −0.337. A partial gate is
worse than either extreme, and it owns the catastrophic tails (@4 −25.72 on backf@300, @6
−20.33) that never-promote does not have (worst −3.06). Mechanism, read from evictFromMain:victimQueue starts at PROBATION and only moves to PROTECTED once probation is exhausted, so
under a high-but-finite gate the few entries that do reach protected are effectively removed from
the victim pool indefinitely — protected never approaches its maximum, so demoteFromMainProtected
rarely fires to release them. Never-promote avoids the pathology by keeping protected empty. Any
future proposal to gate promotion must answer this, not just pick a better threshold.
Reconciling failed-probe returns
undoRemaining accounts for issued integral commands; the cache applies them through budgeted
transfers of indivisible entries. A full sample can replace an unapplied remainder from each
stride, so a failed return can finish short and rebase the next probe. This can accumulate beyond
one entry: with maximum 10000 and nine entries weighing 1100, a public-operation witness returns
from 7800 toward base 100 with commanded decreases of 3000 + 3000 + 1700, but applies only
2200 + 2200 + 1100. The shortfall is 2200, two whole entries. A controlled unweighted witness with
8192 residents, a 128-CPU buffer configuration, a queued executor and buffered replacements loses
914 entries over two sample boundaries; its 16-CPU control returns exactly. These establish
reachability, not ordinary-runtime prevalence or hit-rate harm.
Two candidates were priced on 2026-09-13 against the real cache:
- Bounded reconciliation retains the base and derives later strides from the actual window,
while the original command ledger limits the number of return samples. Final carry remains
best effort. It recovers the two lost 200-unit remainders in a 25-entry/400-weight witness
within the same three samples, so recovery need not prolong retreat. - Acknowledged return retains the base and preceding window, ending on observed arrival or
non-improving distance. Completion is checked before rate-reference updates to preserve the
ordinary arrival sample's anchor tracking. Small positive progress can keep this return alive
longer; the nine-entry witness needs a fourth sample to recover the whole 2200-unit shortfall.
Ten weighted cells from six traces (MetaCDN_rprn, metaStorage_block_traces_1, alibabaBlock_128,tencentBlock_11784, cloudphysics_w50 and w99, 16 MiB to 1 GiB), paired at admission seeds 1–8,
gave mean object-hit-rate changes of -0.050 to +0.060pp across both candidates. Some small
effects repeat: Tencent at 64 MiB gains about 0.04pp; w50 loses 0.016/0.010pp. Neither candidate
earned adoption. After a fixed write-flood prefix exposing the unweighted shortfall, a switch to
two million unit-weight w99 requests costs the candidates 813/834 additional misses (about 0.04pp)
at all eight seeds. Exact return does not imply a better allocation after a workload change.
The acknowledged arm reached 14 return samples against a maximum of three in the baseline and
bounded arm. On metaStorage at 64 MiB, return duty rose from 3.30% to 7.26% of adaptation samples,
with no resolved hit-rate gain. These are sample fractions on each arm's evolving path, not CPU
time. Both arms still often stop short on weighted traces when the final remainder cannot move.
JOL estimates on JDK 26 put the added retained state at 8/16 bytes per density cache, with no extra
objects. A 32-fork uninstrumented JMH screen could not resolve its declared 5% threshold: baseline
spread exceeded it. The acknowledged weighted arm was slower in all four blocks (descriptive mean
-4.68%), an unresolved cost signal rather than a no-cost result. Three unit-weight trace projections
and all 87 gate and
bundled stress cells at eight seeds matched baseline's checked cache outputs and admission
digests; shared baseline bar failures remained. This does not price weighted behavior by itself.
Retain best-effort undo on the measured tradeoff. Reconsider with a repeatable quality benefit
that pays for additional state or retreat time; neither incomplete physical arrival nor a
percentage of displaced capacity establishes that benefit.
Entry-count tier selection for weighted caches
resized selects density above a maximum of 4096 in the weigher's units, however few entries the
cache holds, while AddTask sizes a weighted cache's sketch from its live entry count. Density's
period min(4·maximum, sketch.sampleSize) matches standard reactive's when the sketch cap binds;
byte units alone do not guarantee that. The comparison prices the whole controller, including
density's floor, probes, audits and anchor.
The 2026-09-13 comparison used 18 byte-weighted oracleGeneral cells (MetaCDN_rprn,metaStorage_block_traces_1, tencentBlock_11784, alibabaBlock_128 and cloudphysics_w99, from
4 MiB to 16 GiB) replayed through a cache configured like product.Caffeine, with three unseeded
repeats per climber arm and static windows at 1/5/20/50/80% as the reference. Entry counts
are medians sampled every 65536 demands after the first half-full observation. Small differences
need seeded pairing before motivating a change. The fixed-window grid is a coarse reference,
not an upper bound on adaptive gains.
No consistent reactive advantage at low resident counts. On the seven cells with medians of 20–389,
reactive − density reads −0.21..+0.27pp with density ahead on four, and density trails the best
static window by at most 0.66pp (one cell reads above it), against up to 1.57pp on the cells holding
800 to 72k entries. Larger observed differences do not follow a simple count threshold: reactive +0.96
on MetaCDN_rprn @ 4 GiB (800 entries) and +0.45 on tencentBlock_11784 @ 256 MiB (11k), −1.45 onalibabaBlock_128 @ 1 GiB (72k), where its window median falls to 12% against a best static 80%.
Retain the configured-weight threshold on this evidence. Grow-only selection from the first
request changed object hit rates by −0.21..+0.26pp across the 18 cells. On five cells whose counts
straddled 4096, delaying that check until half occupancy produced the same first switch. Following
the count both ways on those five cells switched 23–13,434 times per run and changed hit rates by
−1.71..+1.10pp. Each switch replaces the controller and resets its sample and carry; the experiment
did not isolate those effects. Reconsider with a reproducible benefit that includes switching costs.
Other pricings for the walk's two interior exits
The split's own dead arms, all measured N=8 seeded. Do not re-derive either.
- A reversal bar with no base-rate term at all (
revdev,min(5pp, k·dev)) is dead across its
whole range, at both ends for different reasons. At k=3σ (noiseBand) it is bit-identical
tofloorrevon every seed of every cell measured — gain and loss alike — because wherever a
floor at 3σ binds,max(fraction, 3·dev)is3·dev, so the two derivations are one bar. At
k=0.5σ it resolvesarc_S3(+0.04) and then costswhisper_mod_a12−2.35 andcrashnoise_a12−1.77. Nothing in between serves:crashnoiseneeds ≥5pp at its scatter
whilearc_S3needs ≤1pp at its own, and the required multiples are ordered the wrong way. The
mechanism is general — a scale-free bar is necessarily small where the scatter is small, which
is exactly where the shipped absolute is load-bearing. A base-rate term must survive on the
reversal as a lower bound, which is what shipped. floorrev/revfl300(floor both the reversal and nothing else at3·dev, under the 5pp
cap): the right shape at the wrong multiple. It buysshieldtrap+0.49/+1.81/+1.40 andmarggate'sslowswap_r20+1.81, and costsarc_S3−0.45 — 38% of whatAUDIT_BAR_FRACTIONwas introduced to buy, on the cell it was bought for — pluscp_w050−0.27.
At the shipped 0.45σ the same arm costsarc_S3−0.01 and gives theslowswapprize back. The
whole distance between those two outcomes is one multiple inside one family, not two mechanisms.revfl100(the same floor at 1σ):arc_S3−0.42, i.e. the fullfloorrevloss. The cliff
between the shipped 0.45σ and 1.0σ is why the level is not worth re-deriving.
Marginal steering on both sides
The round-2 kill pointed at ln(d_windowTail / d_probationTail), marginal on both sides,
keeping probation as the denominator while measuring its margin rather than its bulk. It was built
in two sizings (margboth, proportional δ·W against δ·P; margeq, equal capacity on each
side so the denominators cancel to ln(H_wTail / H_pTail)), and both are dead. But the result
that matters is not the arms.
Neither beats the one-sided form. Nine real cells at N=8: ship +0.00 / 0 losses / Spearman
+0.950 / 21.3pp level error; marghalf +0.83 / 1 / +0.983 / 11.3pp; margboth +0.66 / 3 /
+0.967 / 14.0pp; margeq +0.69 / 1 / +0.967 / 13.0pp. A second band on a second deque, for
strictly worse numbers. The two sizings land together, so proportional-versus-equal-capacity is
not the question.
The frontier. Averaging the two slowswap rows against that screen, the four denominators
trace one monotone inverse curve: marghalf 30.40 / +0.83, margeq 31.74 / +0.69, margboth
34.92 / +0.66, margtail 36.92 / +0.43, ship 41.32 / +0.00. Spearman −1.0, slope ≈16pp ofslowswap per 1pp of corpus. A denominator does not escape the trade, it only picks a point on
it.
Gain is not the lever, and the family has a ceiling. marghalf swept over gain 0.125–1.0 onslowswap_r20 reads 32.90 / 30.38 / 30.02 / 29.65 / 36.20±5.23 / 31.12: non-monotone, unordered,
its one high point a lottery. And margtail, the family's best slowswap point, reaches only
37.52±0.18 at its best gain. Across the ~14 (denominator, gain) points measured on that row the
best mean is 37.52 and the best single seed anywhere is 38.67, against a ≥40 bar and
ship's 42.19.
So the thread does not close because arms failed; it closes because the family has a measured
limit short of the gate over both of its free parameters. What remains untried is outside it:
every remedy so far picks a different denominator, while the slowswap diagnosis names probation's
composition under heavy inflow. A regime-gated fallback is a different shape from round 1's
rejected starvation-gated one, but price it against ~+0.24pp, the corpus prize the frontier leaves
at the bar, not against +1.22.
Marginal steering: the signal is real, the controller is not shippable
The gated arm is the family's best point and its residue redirected the thread. marggate
(fall back to the average law where probationDensity exceeds mainDensity, a trustworthiness
test on two terms Reading already computes) reads +0.690 mean over the nine cells with one
−0.04 loss, rest-point tracking Spearman +1.000 with 12.2pp level error against ship's
21.3, and sits at the same slowswap robustness as dead margtail for 60% more prize. Its
structural false positive: on flood_j100 a rotation exceeding protected demotes every promoted
key before reuse, probation reads richer than main with no inflow at all (47 of 48 steering
samples), and the arm is the average law there. It fixes slowswap_r1 (5.6pp below LRU →
38.24 ± 0.18, +1.45 above, deterministic) and not r20 (−6.9 → −0.95, bimodal); neither meets
the ≥40 bar. The residue is recovery, not detection: per-seed on r20 the gate fires 11% of
steering samples in the good basin and 9% in the bad one, firing rate does not predict the
outcome, and what separates the basins is where the window ends up (30.5% against 20.0%) — the
depressed-window thread's territory. Dead within the gate, do not re-derive:
a run-to-enter requirement (streaks 2 and 3 both lose to 1), an exit latch (non-monotone, never
lifts r20 above LRU by more than 0.00 over 1/2/4/8/16), and the gate ratio (the two rows want
opposite settings: 1.0 gives r1 +1.45 / r20 −0.95, 0.6 gives r1 −0.62 / r20 +1.01).
margrest (the marginal law only inside a rest band, |steeringError| <= REST_BAND) solvesslowswap_r20 outright (42.20 ± 0.05 against ship's 42.19, deterministic) and destroys the
prize (nine cells +0.133 against marghalf's +0.833, w098 collapsing +4.33 → +0.35). The
mechanism is the wall the record now names everywhere: w098's win is a rest-point
correction, but a large one — the valuable corrections are long relocations, and a long
relocation is indistinguishable from a transition by command magnitude. No size-gated rule can
work, and the same fact underlies the frontier: the signal cannot separate "a big move because
the rest point is wrong" from "a big move because the workload changed". Two structural facts
fell out: the average-law error is bimodal (near zero at rest or large in transit; bands 0.5
through 0.1 are bit-identical), and on the bad r20 seeds the final third is a park, 95 of 103
samples at a 16.9% window with one audit armed — the marginal law digs a hole, the anchor parks
in it, and nothing in a steering law climbs out of one. The stillness-coupling blocker is
repaired entirely by halving the gain (a threshold on the stable band, not a monotone trade;
0.25 is worse than 0.5); the probation-denominator defect under heavy inflow is what the
denominators above could not fix.
External corroboration (OSDI'26 "Learning-Augmented Heuristics", Xia et al.): their model's
feature importance puts 75% on the three queues' hit-position histograms — per-region
hit-rate-versus-size curves, i.e. marginal-value estimators — over 4140 production traces.
Corpus-scale evidence for the premise from outside the project. d_tail is one bin against
their 20 per region, so bin resolution is the knob if a future arm reads ambiguous; nothing
else in the paper transfers (it is a static per-trace configuration selector). Screen any
successor on rest-point tracking before spending a battery: Spearman(meanWindow, peakWindow)
over the nine D3 cells reads ship +0.917, marghalf +0.983, margtail +0.367, and a candidate
that does not beat +0.917 is buying stability with tracking — trap wins paid for by corpus
losses.
The thread was closed against a corpus prize under a point. The LLM prefix-cache cells (§3, terrain
classes) are real traces where the average law's rest error is most of a 3–7pp loss: on the static
anchor it is bistable on Thinking@16384 and rests at the floor or near 40% on Coder@65536, where the
marginal form rests near 71% and at the ceiling against 80% peaks. A re-opened thread prices
against /climber-gate's prefix-cache row as well as slowswap.
Verdict corroboration, and the delayed entry stride
Verdict corroboration, the smoothed reference (corrob2; the raw form died first). A
pulled-back confirm must reproduce its own claim (Walk.bestRate) in the smoothed rate within a
confirm-streak of samples, or the park relocates to the walk's end. It does what the raw form
did — crashnoise_a12's low basins lifted (+1.03 mean, the row near-deterministic at 62.7) andwhisper_mod_p12 +0.64 on 8 of 8 — and fails whisper_mod_a12 the same way (−1.67 mean, seven
seeds at −1.98, seed 7's low draw the one gain): at that amplitude the EMA still carries ~40% of
the modulation, the park's first samples land in the trough, and a correct position fails its
own claim. The reference is not the defect; the depth is. The smoothed form also adds
relocation damage the raw one did not have: demoflood −3.64 on 8 of 8 and crestpast
−4.2 / −2.9, where the lagging EMA reads a fresh crest park as unproven and revokes it to the
walk's end, re-deriving the exact overshoot the verdict repair removed, plus straywall2
−0.78. Dead. Both measurable references die by the same symmetry, and the third (the fallback
position's own rate) is unmeasurable without going there; the corroboration family is closed.
The delayed entry stride (delayentry, item 8's blank). Freeze the base at the arm and
take the entry stride on the next sample, the WaveCounter comparison. The prior held and the
mechanism sharpened: the entry's timing is itself phase-load-bearing. absolve reads −15.7
on both seeds, back at its pre-repair value, because the lure-paced escalation needs the walk
to arm in the off phase and stride before the on phase returns; straywall2 −0.88;norank_rep_r6's escape lottery reshuffles violently (seed 3 +48.6 against seed 7 −59.8, mean
−2.2); against blindlock_blind +5.8 and demoflood +0.7, which do not buy that. Dead; the
record's blank is priced in the direction the prior expected.
The marginal band priced against main's average
ln(d_tail/d_main), the marginal band priced against main's average instead of its probation
margin, was the named answer to the one marginal-steering residue that halving the gain does not
touch. It is dead in both directions, and the reason was derivable from §4 before it was built.
It halves the prize. On the nine D3 cells margtail is +0.43 mean with four losses
(P9 −0.41, P10 −0.55, w081 −0.64, w060 −0.75) against the full form at half gain marghalf,
+0.84 with one.
It fails the gate it existed to pass. Seeded at N=8, slowswap_r1 reads 36.31±0.92 andslowswap_r20 37.52±0.59 against a ≥40 bar and ship's 40.44 / 42.19. It recovers ~63% ofmarghalf's slowswap residue, not the ~89% the static rest-point anatomy implied (1.69 against
14.63pp), and still lands 4.1–4.7pp low. Two side results: the declared trade was wrong, sincetrickle_s7 improves (72.59±1.30 against ship's 70.70±3.63); and widepin is the arm's one
real win, 56.32±7.00 against ship 49.58±13.73 and marghalf 48.12±14.03.
The mechanism, which is the part worth keeping. A steering denominator must move with the
window or the rest point stops depending on the window. d_prob moves (a grow squeezes main and
demotes protected entries into probation); d_main is dominated by the protected core and barely
moves, so the error degenerates toward ln(d_tail) − ln(const). Measured directly as rest-point
tracking across the nine cells, Spearman(meanWindow, peakWindow) is ship +0.917, marghalf
+0.983, margtail +0.367, with mean |window − peak| 23.0 / 14.9 / 19.2pp: margtail's
middling level error is range compression, not accuracy, and it rests at 12.6% and 13.0% on the two
cells whose optima are 40% and 70%. That is the same fact §4 records for the sibling mechanism, an
up-probe's verdict being priced against probation "rather than its average", and widepin is it
read from the other side, since an arm that does not track cannot chase a pin. An arm that wins
on the traps and loses on the corpus is buying stability with tracking; screen the tracking
statistic before spending a battery.
Repairing the reactive law instead of replacing it
Can the reactive law be repaired instead of replaced — specifically by giving its reversal a
noise band, as nextStride gives the walk's? Four forms were built and measured against both
incumbents: a flat 5pp band, the walk's priced band (clamp(3·dev, 5pp, 15pp)), the same with
the density tier's 2% floor, and probeEnding's base reference (measure from the run's best
sample, ratchet on improvement) with and without aging. All dead, and the reasons are structural
rather than tuning.
- A band on the reactive reversal, any form. Real traces: mean +0.16pp vs reactive over
seven cells, range +4.06 (cp_w061@64k) to −2.81 (arc P8@64k), never closing the gap to the
shipped machine (mean −0.73). Constructed families: five of six lose, mixture d050 long
−25.89, mixture d025@32768 −25.29, straywall2 −12.24, whisper −10.53, demoflood −3.37. A band
buys commitment, and commitment without an adjudicator is a direction chosen by early noise
that nothing can revoke — the same wall v7's travel budget and v9's slam-continue hit from the
probe side. The reversal is the reactive law's "confused but never persistently wrong"
property; a band spends exactly that. - A band with no window floor is catastrophic, and this is a standing constraint on the tier.
BoundedLocalCache.decreaseWindowfloors the window at one entry; the 2% floor is climber-side
inReading.floorand belongs to the density tier alone. The reactive tier has never needed
one because a bold driver cannot sustain a run. Widen the band and the window walks to ~0,
where TinyLFU refuses every new entry against an established victim: corda 30.96 → 1.13.
Any future change that lengthens a reactive run — a band, a longer period, a confidence gate —
must bring a floor with it. - Aging the base reference (decay the peak toward the live rate at 0.2/sample): inert on four
cells, −0.5 on two, and on arc P8 it collapses to the un-ratcheted band's value exactly
(42.57 vs 46.90). Third independent kill of reference-aging, after the anchor claim's two.
Two results from that study are not kills: the magnitude deficit is
partly a reversal-rule problem rather than purely a signal one, and the trap gallery's value is
partly variance reduction that N=1 comparisons hide.
Guards derived from the control-theory map
The control-theory map (§2.1) suggested two guards that could be derived rather than tuned.
Both were built behind variant knobs and both are dead. The holdout frozen for the study was
never spent — neither arm reached a ship gate.
- Freezing the walk's deviation reference at arm (
devfreeze):walkInteriorBarprices a
starvation probe's crash abort at3×rateDeviationEma, and becauseupdateRateReferencesruns
before the probing branch, the walk's own transient inflates the very statistic that decides
whether to abort it. That description is accurate; the inference that it is a defect is not.
The feedback is load-bearing: on a dosed workload the deviation at arm is a pre-dose
value, so a frozen bar sits too low and the walk crash-aborts inside the dose — exactly the
failure the adv3 probe-side pricing exists to fix. Measured:mixnoise_a1060.70 → 55.97
(−4.73, spread 0.01, fails its ≥58 bar — the pricing's own prize row) andcrashnoise_a12
63.59 → 62.18 (−1.41, spread 0.01, drifts below its 63.6 floor);devfreeze_nocapis worse
(widepin−2.48,phases_d050−2.75). The live deviation andPROBE_BAR_CAPare a matched
pair — the feedback keeps the bar above the weather while the walk is exposed to it, the cap
bounds where that would run away. Don't freeze it, and don't drop the cap. - A within-sample confidence dead-band (
deadband<z>): suppress a steering step whose error
is insidez·√(1/H_w + 1/H_m)(the delta method on two counts — free from counters in hand),
as a derived answer to F-2 and, since a step never taken cannot move the window, a derived
stillness band. Dead twice over. It barely binds: at a 4×C sample period each region earns
thousands of hits, so 3·sd is ≈0.04 nats and the step that commands is ≈0.13% of the maximum —
measured mean +0.03 across 40 non-bimodal cells, two cells past ±0.2. And its motivating
case is not a significance problem: onposjam_d0, 0 of 121 super-band moves have an
error inside its confidence interval — the jam's motion is statistically real and window-
irrelevant, so no confidence test can act as a stillness measure (the dose-matched flat
control is the mirror image, 5 of its 11 super-band moves inside the CI — it binds exactly
where a defense is not needed). Theposjamresidual stays a stillness-measure problem, not
a noise problem.
Ageing an off-anchor claim instead of freezing it
Both forms of the thread's other named fix direction — letting an off-anchor claim age toward
the live rate instead of freezing — fix the target and are dead for different reasons. The landed
fix re-seeds the goal metric on a discarding stand-down instead.
- Symmetric aging (
ageclaim:anchor.ratedecays towardrates.smoothedwhile the
window is away and settled): −11.9 on theslowswap_r20ramp control, on every one of eight
seeds (42.14 → 30.29), while fixingslowswap_r1exactly as the landed fix does. A claim that
chases the live rate upward can never fall avetoMarginbelow it, and that comparison is the
only thing that moves the anchor to a better position: the anchor freezes at a position the
window has left for the rest of the run (trajectory: park samples 69 → 1,AUDITCONFIRM1 → 0,
down-probes 7 → 0, steering 128 → 238). Slowing the decay to 0.05 hides it on that trace without
changing the mechanism. - One-sided aging (
agedown: decay only while the live rate sits under the claim): passes
both slowswap arms, battery mean −0.067 with no seeded mover, and still dead — it disarms the
guard rail. The rail needsVETO_STREAKconsecutive rail evaluations ofsmoothed < rate − vetoMargin, and aging closes that gap at the same raterateDeviationEma
decays, so the shortfall and the margin shrink together and the streak never completes; both
rail pins fail
(guardRail_sustainedShortfall_vetoesBackToAnchor,guardRail_marginTracksNoise_silentWhileWideThenVetoes). The rail is measured near-dead on this
battery, so the live cost is small — but a compensating fix that trades a documented mechanism is
the wrong shape when the same defect has a source-level fix that leaves every pin standing.
Park persistence, and other confirm-side levers
- Unconditional park persistence (a parked confirm never crash-releases): captures the full
w097/whisper-p6 prizes but converts the tracking controller into hold-and-retest exactly
where tracking is the cure — regimeramp 54.8 → 48.5, widepin 54.8 → 48.5 (below its bar),
phases@8192 −4.1. The bounded fresh-park shield (one initial audit wait) keeps the prizes
with all three at ship values — the shield length is load-bearing, not a tuning residue. - Deviation-priced audit margins generally (the F1 mitigation table:
flat/ema/cap
margin modes recover only 25–35% and trade the rail for the audit) — superseded by the
run-length streak; kept here so nobody re-prices the confirm by any multiple ofrateDeviationEmaagain.
Blending the two steering laws
- Confidence-gated steering blend (
(1−w)·density + w·reactive,w = W·min(1, |ΔHR|/devEMA)):
the T1a form. Whisper −1.1, jam-control −3.4, and zero rescue on its own targets — the
density attractor undoes uncommitted per-sample nudges exactly as §T1's coin-flip analysis
predicted; a confidence gate does not change the physics. Committed walks (probes, audits) are
the only instruments that cross basins. - Settle-then-judge (hold a walk's final position ≤5 samples, adjudicate by live-margin rate):
demoflood +6.6 but phases −8.4/−11.5 dose-responsive — an exogenous phase rise credits the
held position (misconfirm) and plants a wrong anchor. Any goal-metric confirm must survive the
phase-alternation families before shipping. - Goal-metric senior confirm with margins frozen at arm: no effect — the margin freezes an
inflated deviation and the anchor reference is stale by construction on noisy traces (the 3·dev
ratchet bar never fires at dev 0.03–0.07; measured 130-sample-stale on cp_w097_16384). - Dwell-gated anchor ratchet (
auditClock.stillSamples ≥ 2before the anchor may move): no
co-tenant help; widepin re-destabilized.
Audit attribution: headroom without a deployable rule
These findings constrain future audit and retention changes. Reusable demand inputs and commands
are in /audit-regret's targeted controls.
Experiment reports, raw runs and controller-specific replay instrumentation are transient;
the design conclusions and controls below are the lasting record.
Paired replay at 0494074819b08f8c6076f40a544c882beeeac2d0 found material avoidable
misses, but neither tested decision-time discriminator justified a production candidate. The
selected real, trap and lifecycle panel used the real cache, fixed-probation geometry, admission
seeds 1–8 and request/contest-indexed randomness; it does not establish corpus-wide saturation.
For the comparisons below, C is capacity and the horizons are demand requests after the decision.
- Retention can repay an initial loss. On
jumpslideat C=8192 from the shipped start,
routing the first unparked starvation confirmation through the existing park path costs
4,176 additional misses over 16C (−3.19pp), then saves 93,460 over 128C (+8.91pp) and
193,000 over 256C (+9.20pp). Each sign holds on all eight seeds. These are paired means for
one intervention, not a rule for recognizing a persistent reuse band. Thecp_w050case in
§3 likewise establishes a feasible gain without establishing a false rejection. - A pre-existing decline does not justify tolerating the first audit crash. The screen
snapshots whether the arm sample already fell by at least 5pp, then grants the existing
bounded three-sample retry at the first crash. At 128C it selectscp_w050's +0.99pp and
also selects −0.73pp after whole-cache invalidation and −1.71pp after hot-key invalidation
on the 55%mainsatplant, each on eight of eight seeds. Weather preceding the walk is
evidence about cause, not sufficient evidence for continuing it. - A raw shortfall does not establish an obsolete return claim. Rejecting a passing retest
when its raw rate is below the frozen claim by 1pp and has just fallen by at least 5pp
costscp_w0810.23pp andcrashnoise_a127.73pp at 128C, on all eight seeds. Both screens
were specified from baseline observations before their paired outcomes were disclosed;
neither was repaired by adding predicates to exclude its losing controls. - Some hindsight verdicts require unseen future information.
jumpslidefrom a 60% plant,
with only its rotation period changed from 13 to 26 nominal samples, has 425,984 identical
initial requests. Every first audit crash occurs at request 262,152, with identical cache,
controller and admission history in the two futures. Tolerance at 256C averages −0.65pp
for period 13 and +5.61pp for period 26; five seeds require opposite actions. The latter
wins only 5/8 seeds, below the study's 7/8 consistency requirement. No history-only rule
can distinguish those futures at that decision. This does not rule out acting later, once
the unchanged cache has received distinguishing evidence. - Lifecycle misses can be unavoidable. At C=8192, empty caches with fixed write TTL covering
C requests on a request-based clock and loading only on demand misses matched an independent
TTL-only reference onmainsatand a 512-key hot-set/scan control. The reference's live entries
fit in C, and neither cache performed a size eviction.
Resizing cannot reduce those misses. This is a scoped lower bound, not an inference from
underfill or an expiry counter, and it does not price transfer CPU or cover other lifecycle rules.
Keeping an allocation fixed for the whole suffix also suppresses later audits, so its gain
cannot be charged solely to the first excursion. A short-horizon gain can reverse through later
scheduling: first-crash tolerance on cp_w100 reads +1.16pp at 16C and −1.88pp at 256C.
No production candidate or full adoption-gate pass followed from this study.
Later retention: useful evidence before the benefit is lost
The focused follow-up at the same commit used independent unchanged-baseline prefixes at
1, 2, 4, 8 and 16 nominal samples after the first unparked starvation confirmation. A nominal
sample is 4C requests. Each action uses the first eligible ordinary density-steering decision
in its registered one-sample window, after higher-priority routes; no earlier alternative park
supplies the later state or evidence. Endpoints remain fixed to the original episode.
- Waiting need not consume all the benefit. At the common 128C endpoint on
jumpslide,
current-position parking at delay 8 saves 67,242 misses (+6.41pp, 7/8 seeds), about 72% of
early parking's benefit. Delay 4 saves more than the early action, so remaining benefit is
not monotone. At delay 16, parking the current allocation saves only 0.36pp, while returning
through the real capped-transfer path to the earlier confirmed allocation still saves 6.46pp. - A later signal can distinguish some harmful parks. The two initially tested
earliest-trigger rules failed controls. A single timing hypothesis was then frozen from
discovery: evaluate persistent regional support only at delay 8. The signal, S1, required both
current and preceding baseline samples to stay within 0.02C of the confirmed window, with
window hits per sampled request no more than 1pp below the confirmation's contribution and raw
hit rate no more than 5pp below its raw rate. Only the current sample's ordinary integer density
command had to shrink. Confirmation usedjumpslideworkload seeds 11/13 and admission
seeds 9–16, held out until that rule was frozen. All ten selected actions saved misses at 64C,
128C and 256C. The complete seed-13 cohort has seven gains and one abstention: +7.46pp at 128C,
retaining 93% of early benefit.
Seed 11 has three triggers, four abstentions and one missing eligible window; it is not a
complete eight-seed pass. The signal rejects seed-11 actions that would lose 2.39 and 1.26pp. - Missing eligibility is not discrimination. Phase durations 20C/24C supplied no eligible
delay-8 windows. A separately registered 18C/22C extension supplied 13, all rejected by the
frozen signal; ten forced parks lose misses at 128C, eight by more than 0.5pp. Those individual
paired losses are valid controls even though missing seeds prevent an eight-seed group gate.
Across the 48 fresh registered cases, only 28 had eligible observed decisions: ten selected
gains, thirteen avoided losses, three missed gains and two neutral abstentions. The signal
recognizes a useful subset, not every retention opportunity.
This establishes a useful tested later point, not a continuous safe interval, a general timing
optimum or a production improvement. The 104-baseline, 403-pair study verifies feasible loss
avoidance and baseline-only information; repeated candidate behavior and the existing adoption
gates must be measured separately. The real cp_w050 control has no qualifying episode on
any seed, so it supplies no retention-effect or discrimination claim.
The resulting isolated candidate kept the first pending confirmation's independent window,
raw-rate and window-contribution references. Later confirmations did not replace them or postpone
the eighth-completed-sample deadline. Assessment required final ordinary steering, no incoming
transfer adjustment and no pending retest. Priority routes consumed the deadline without a retry;
shifts, walks, parks and releases did not cancel it. resetSample canceled it, and an actual
maximum change replaced the tier. A true S1 planted the current window/current smoothed rate,
used the existing park(32) shift shield, and preserved the previous step rather than committing
the unexecuted integer shrink. The episode then expired and a later confirmation could rearm.
It preserved the existing router, audit, release and retest machinery. First-episode validation
matched the diagnostic across 104 cases: 67 eligible decisions, 24 ineligible expirations and
13 absent episodes. In the canonical seeded-stream battery, 92 of 94 cells had identical counters
and trajectories. jumpslide gained 2.63pp (five gains, three ties), and whisper_quarter moved by
less than 0.005pp per seed. These are separate admission
schemes and cohorts; their deltas are not pooled. Three historical bars (deadphase,sidecliff's noaudit control and whisper at 16384) failed identically on the study baseline
and candidate. The paired comparison found no new behavioral regression; the literal historical
bar results remain separately qualified.
Longer continuation still matters. On the previously held-out jumpslide workload seed 13,
admission seed 14 parks once, saves 25,085 misses at the original 128C endpoint and 8,926 at 256C, then
ends the 260-sample trace 234,178 misses behind (−2.75pp over the whole trace). Its complete
hit sequence matches the earlier one-action replay, so repeated rearming did not cause that
reversal. All 120 registered TTL/invalidation cases were unchanged, but the matched no-expiry
period-26 jumpslide control from a 60% plant lost 35,363 misses on one seed (−0.83pp; the
other seven tie). Its initially beneficial park changed later audit/release behavior and ended
at a ceiling park. A retention action includes those later consequences, not just the time it
keeps the original allocation.
Keep all known full-continuation costs visible, including losses allowed by the group screen:
| Workload / admission seed | Retention parks | Whole-trace extra misses |
|---|---|---|
jumpslide, workload seed 13 / admission 14, 260 samples |
1 | 234,178 |
jumpslide, workload seed 13 / admission 10, 260 samples |
1 | 64,698 |
jumpslide, workload seed 11 / admission 14, 260 samples |
1 | 104,524 |
jumpslide, workload seed 11 / admission 15, 260 samples |
2 | 13,505 |
period 26 jumpslide, 60% plant / admission 1, no expiry |
1 | 35,363 |
These are request/contest-PRF, direct-executor results, not canonical seeded-stream or ordinary
ThreadLocalRandom expectations. Both seed 13 losses match their earlier single-action continuations.
The period 26 control's seven peers tie, for a group mean of −0.103769pp. The registered rejection
requires at least 0.5pp mean loss, C/4 mean extra misses and 7/8 negative seeds; none of these groups
meets that rule. That does not erase individual costs or create an any-negative-seed rejection rule.
The candidate was not adopted. On the tested Temurin 25/macOS arm64 JVM, the Java
11-compatible implementation added 32 retained bytes per density-tier cache and no additional
retained object. Direct density-tier JMH measured about 5ns more per ordinary sample and 3ns per
sample over the controlled retention cycle. Whole-cache throughput remained inconclusive: baseline
fork variation and wide paired intervals did not resolve small costs.
Neither those intervals nor a cold-path location establish zero cost or an unacceptable cost.
This is a demonstrated later opportunity with unresolved adoption gates, not saturation or a
production improvement.
The bounded runtime follow-up also ended inconclusively. Its 32 source-identical baseline forks
(four pairs in each of four whole-cache cases) resolved neither the registered ±2% sensitivity
target nor a directional build effect. They were not candidate regression measurements. The
archived 2% sensitivity floor, 5% noise ceiling and 0.01 B/op flag floor are diagnostic rules;
no numeric engineering acceptance budget was found. That budget and a qualifying measurement
setup are prerequisites to new candidate classification. No additional candidate timing or
reserved-holdout outcome checks followed from this unresolved result.
A later integration-only follow-up passed 1,499 unit/integration invocations and one
five-minute fuzz campaign (2,228,167 executions), with no candidate defect found; specific retention
branch exposure in fuzzing was unmeasured. One of two ordinary-admission/direct-executor observer
runs naturally armed, parked at sample 72, audited at 99 and released at 109. Neither of two default-
executor observer runs parked; clean counterpart action counts were unmeasured. Default mode had
one demand producer and background common-pool maintenance, not concurrent demand producers.
Source stripping and controlled observation checks do not establish exact concurrent histories
or CPU neutrality. A same-maximum policy call still runs maintenance and may age a completed
sample; tier preservation does not mean all controller state is unchanged. Likewise, quiet
completion and lifecycle operations add no cancellation rule but may process prior observations.
The 17 real/floor cells and their registered quarter, repeated and planted controls made 42
paired cells at eight admission seeds each. All counters and trajectories matched, and the
retention action never fired. The existing recovery bars held, but dormant behavior supplies
neither a real-corpus gain nor evidence that an active retention decision is safe there.
S1 was closed without adoption on 2026-09-08. Its source/test snapshot was discarded; the
decision-time findings, known losses and reusable workload controls remain the lasting result.
Runtime acceptance and final reserved checks were not established: ARC P3 was unspent and the
referenced hash-pinned merge specification was not located. Closing the candidate does not turn
those unknowns into a performance rejection or establish that no useful improvement remains.
A future investigation can recreate an experiment against the current source using the targeted
controls and §6's decision-replay protocol, with its own source binding and validation.
6. Methodology (the discipline that kept this honest)
product.Caffeinein the simulator is the arbiter — the sim reference climbers diverge from BLC
(ps2-vs-x2 history). Randomized admission gives ~0.1–0.8pp run-to-run noise: N≥5 runs for any
claim under 2.5pp, absolute pp only, no net-sum headlines over noise cells.- Where that noise comes from, and why some cells are bimodal. The source is
BoundedLocalCache.admit: a candidate at or aboveADMIT_HASHDOS_THRESHOLDthat does not beat
the victim is admitted on a 1-in-128ThreadLocalRandomdraw (the HashDoS defense). TLR seeds
from system entropy per JVM, and the policy runs.executor(Runnable::run), so this is the only
live nondeterminism. The experiment harness can replace that contest-time draw with a seeded
stream, which makes each arm independently reproducible. Equal seeds do not make two arms use a
draw on the same request when their contest schedules differ. The arms are paired by seed, not
by request-indexed common random numbers, unless both the draw count and request-index digest
match. On most cells the draws average out
(measured over five batteries: 28 of 47 cells vary by <0.05pp run-to-run, 37 by <0.20pp). On a
constructed bistable trap an early draw can tip the region composition into the other attractor
and the climber's own feedback locks it in, which is what a lottery cell is: 6 cells
(phases_d050,widepin,shieldtrap_s7/s13,balloonflip,crashnoise_a12) vary by
1.1–5.5pp with the code held fixed. Consequences for reading a gate: a mover inside that set is
evidence of nothing, interleaving arms limits temporal drift but does not make their draws
common, and a single-cell regression is adjudicated by basin count at N≥10, never by a mean at
N=2. - Adaptive N is sound and ~60% cheaper: N=2 everywhere, N=5 where |Δ| ∈ 0.5–2.5pp or spread >0.5.
- Lengthen a trace with fresh keys, not a plain replay. Replaying a trace back to back lets the
frequency sketch remember every key from the previous pass, which flatters small windows and can
trap the climber there: on the LLM prefix cells a static 1% window gains 4.8pp on To-C@185364 from
that memory alone, and under plain replay the shipped climber stays small where with each pass's
keys mapped onto new ones it reaches LRU or the static 80% window within two to four passes.
Check a multi-pass claim against a fixed policy over one and over N passes before trusting it. - Pair usable-prefix scores with the legacy block scores. A request's usable hits stop at its
first missing block (§3, terrain classes). Preserve exact prefix-chain keys and real request
boundaries, and score both measures on the same paired replay. LRU's equality in the studied
setup must be checked, not assumed for every workload or serving model. Do not silently stop
the access stream at the first miss, add request pinning, or replace historical gate bars;
those are separate replay or acceptance changes. - Test fill transitions rather than naming them from a drop. A hit-rate drop near first
eviction can motivate a fill-transition hypothesis, but admission, sketch initialization/aging,
capacity pressure and workload phase can change together. Isolate those effects before
attribution; a filling cache is not unconditionally equivalent to a large LRU window. - Trajectories (window size per adaptation) are the diagnostic instrument — every fix in §4 was
found by reading a trajectory, not a scoreboard. The debug hook pattern: stderr print indetermineAdjustmentbehind a temporary system property (the simulator forwardscaffeine.*);
no hook is checked in — the 2026-07 study's working-tree harness (variant knob + debug + seeded
admission RNG) is preserved in the
local hill-climber-study workspace (experiment-harness.patch). - Separate information, actuation, and deployability. First ask whether a signal predicts reuse
or ranks counterfactual window sizes on a declared observation stream. Use past-only features,
complete negative labels, cold-start accounting, authentic frequency and equally deep no-feature
controls, and bounded state/work. Check which observations survive generic maintenance; metadata
recovered offline does not make it available there. Reuse prediction alone establishes neither
when to trigger a different mode nor which window direction helps. Then ask whether a controller
moves the live cache safely and converges under clamps, reversals, and phase changes. Only then
price the actual production host for retained state, steady allocation, command/boundary
allocation, and CPU. Passing one layer says nothing about the next: replicated MiniSim passed its
information screen and failed cost, while Compact Shadow passed layout and stopped before quality. - Distinguish issued commands from applied movement. An experiment requiring exact arrival
must keep its decision pending until the host acknowledges the actual signed setpoint change.
Reject overlapping acknowledgements and the wrong sign or magnitude; retain geometry-specific
evidence after a rail-clamped zero; clear it only after a validated nonzero move. Continue a
capped trip from the acknowledged setpoint, not from a nominal percentage or transient queue
occupancy. Tests must pin partial and zero clamps, reversal, cancellation, and exact arrival in
both directions. The shipped undo retains
best-effort arrival; ledger closure alone does not prove arrival. - Use an oracle ladder, not a self-confirming digest. Compare a compact estimator first with an
independent readable model, then request by request with the actual simulator policy under the
same key, frequency history, and admission variate. Check hits/misses, membership, queue order,
promotions/demotions, and admission decisions, not only final hit rate. Accumulate live evidence
independently; copying an expected digest into the measured path proves only that the metadata
agrees with itself. - Low duty cycle is not low request-path cost. A dormant callback, counter decrement, routing
indirection, or branch still executes on every request. The compact studies measured zero steady
allocation alongside material CPU regressions. Dormant observation must be structurally absent,
and even that is only a hypothesis until the exact product splice passes an order-balanced
benchmark. Warm the exact root, actor, probe, and branch shape being measured, and make estimator
work externally observable so dead-code elimination cannot manufacture a win. - Establish benchmark repeatability before interpreting small costs. Use ordered baseline
pairs before candidate pairs, preserve slow forks and report uncertainty. A stable slow fork is
not necessarily an unfinished warmup. If baseline variation exceeds the registered sensitivity,
additional measurements may diagnose it but cannot silently turn it into a permissive pass bar.
Separate cold ns/sample from whole-cache throughput and fixture allocation from new allocation
in an existing cache; state the sample exposure used for any per-request amortization.
Bind the entire cache implementation as well as the controller overlay: class origin proves
location, not that a supplied JAR contains the pinned baseline. Pin the resolved JDK and keep
different runtime cohorts separate even when their benchmark names and launcher strings match.
Check activation outside timing; an initialized sketch can process density samples in an
underfilled cache, while a six-reader/two-writer JMH group does not fix the request ratio. - One clock owns each lifecycle boundary. Host requests, sampled requests, warmup, measure,
travel, and cooldown are different quantities. Express each transition as one absolute request
deadline or one remaining count, not two counters that can both charge the same interval. Pin the
request immediately before and at every edge, and prove that under-full or dormant requests do
not advance the estimator's active clock. - Classify endpoint state before scoring regret. A trace that ends in a walk or audit measures
recovery latency over that horizon; it does not prove a settled rest point. Report the terminal
phase and pending target beside static-oracle regret. For percentile gates, also pre-register the
monotone futility bound: stop once the observed order statistics force failure under every
possible value of the unrun cells. - A holdout must be able to exercise the thing under test.
AUDIT_BAR_FRACTIONismin(RESTART_THRESHOLD, AUDIT_BAR_FRACTION · baseHitRate), so above a 1/3 hit rate it equals the
absolute bar and every arm ties by construction. Selecting by "unspent + density tier + enough
adaptations" is not sufficient: screen the candidate band against the constant's own binding
condition first, or the freeze spends cells on a null result that reads like a pass. - Holdout discipline: freeze a fresh holdout BEFORE tuning, sizes by LRU-only characterization,
score it exactly once at the ship gate — and screen the candidate cells against the constant's
own binding condition first (above). Every earlier holdout generation is SPENT (each spend is
recorded in its study's entry and workspace); the density-climber merge-gate holdout
(frozen with a hash-pinned spec in the merge workspace) remains UNSPENT — no
candidate arm may touch its cells before the final pre-merge gate. - The simulator has NO reference implementation of this climber (the experimental
density-arm-onlySimpleDensityPropClimberwas retired rather than shipped
half-faithful);product.Caffeineis the only arbiter until a faithful reference
lands. The climber-class extraction that was to enable one has happened
(WindowClimber), so a reference is now a port, not a redesign — still unbuilt. - Judge climber terrain on the object-rate column, not the byte panel. The weighted anchor
degenerates bit-for-bit to the classic form on unit-weight traces, so the unweighted record
stands, but the byte track answers a different question and its win/loss counts move for reasons
that are not the climber's. A related trap that inverted a real cell before it was fixed: the
admitter was sized once, when the cache first filled, and never re-sized, while the cache
re-callsensureCapacityon every addition. Since the sketch's reset period scales with its
capacity, a frozen one is TinyLFU aging at near-zero period, which flatters small windows and
washes out at large ones. - No duty or dwell bound can detect the deferral defects, and that result is worth not
re-deriving. The shapes that motivated the search violate the schedule's provenance, not
its magnitude —maxOverduemeasures zero straight through a pin that lasts over a hundred
samples — so no elapsed-time or duty bound can see them. What survives the search is three
oracle clauses, none of them a duty bound: two ownership/evidence clauses (a layer's rung and
wait grow only on a sample that ended its own walk; the wait passes its floor only on a walk that
reached a verdict), which carry zero constants and kill mutations quickly but are regression
value rather than discovery; and one progress clause (a due audit is refused for at most a
bounded number of consecutive samples). The progress clause has real discovery content — on the
pre-fix tree it fails in seconds and fires on exactly the cells the fix moves — but it was not
adopted, because its margin sits one sample under the shallowest defect instance and it is a
mechanism detector whose firings need a human to price, including where the refusal is
load-bearing. Killed alternates: upper duty bounds (every defect is duty-too-low),
sliding-window lower bounds (permanent false positives), and "consecutive samples commanding no
motion" (the progress clause in disguise, or blind at the absolute level a legitimate deep park
requires). - Simulator operational gotchas: the default
admissionlist includesClairvoyant, which wraps
the trace in a materializing reader (pass-Dcaffeine.simulator.admission.0=Alwaysfor LRU
anchors, or run product-only);simulator:simulate's chart renderer wedges headless (run one
size per call); Twitter/IBM holdout projections live at~/projects/merlin-traces/holdout/.
Decision attribution and later retention
Establish the replay before interpreting a verdict. Independently replay the same prefix
into the real cache; compare full policy/controller state, queue order, sketch, node/lifecycle
metadata and pending maintenance at the intervention. An A/A continuation must reproduce demand
outcomes and state. Check observation neutrality separately against the same admission rule with
diagnostic hooks disabled. Do not add cleanup or cache accesses to obtain a convenient snapshot.
If local instrumentation keys admission by request, event phase, ordered candidate/victim and
contest occurrence, verify shared identities and variates and count branch-only contests
separately. The committed seeded-stream harness does not supply that stronger pairing by itself.
Keep study-only hooks isolated; their logs and allocation costs are not production measurements.
Price the defined action. Count cumulative misses from demand outcomes independently of
controller samples. Record actual allocation and occupancy, transfer carry-over, sampled exposure,
decision reason and reference values. Share external demand, ticker and lifecycle schedules on
immutable request indices; regenerate each branch's loads, expirations and completions. Use
several predeclared horizons spanning immediate
damage, settling and repayment. Missing decisions, no-op interventions and censored horizons do
not establish zero headroom. A static optimum or a large expiry count cannot label a false reject.
Later retention is a separate decision-time diagnostic. Its question is whether substantial
remaining savings and a cheap distinguishing signal coexist at a later baseline decision:
- Predeclare an original episode event, a small finite set of later offsets or eligible source
boundaries, skip rules, permissible actions, materiality and controls. Include cases where
retention is harmful. Select eligibility from the baseline prefix at the exact pre-action
boundary, without choosing times from subsequent loss, a future phase label or an alternative
branch's success. - Replay the unchanged controller independently through each later point. No earlier forced
park, hold, tolerance or other intervention may supply its state or evidence. Waiting follows
ordinary adaptation, even if it loses the good position. A hold used to gather evidence is
already a treatment. Log which fields were available before the decision; post-route fields
overwritten by a plant, release or retest are not automatically valid inputs. - From that common state, compare ordinary continuation with one specified bounded action.
Distinguish retaining the current allocation from returning to a previously observed one.
If the position has already been lost, charge real capped transfers, displaced residents and
settling; do not restore an earlier cache snapshot or assume an earlier park's occupancy. - Compare later times at the same predeclared episode endpoints, anchored to the original
event. Keep the misses incurred while waiting and after acting in the accounting. Equal-length
postdecision horizons can additionally explain settling, but moving every endpoint later would
grant the delayed action extra regime time. Report absolute remaining savings first; compare
them with the early-intervention gain at the same endpoint only when that denominator is
materially positive. Such a ratio can be negative or exceed one and is not a probability. - Construct discriminator inputs from baseline-prefix observations alone. Offline outcome labels
may guide fitting on discovery cases; freeze the cheap rule before testing held-out cases and
seeds with both helpful and harmful retention outcomes. Identical observed histories must
produce identical rule outputs until baseline-observable evidence diverges. A difference in
counters is not enough: it must distinguish the better action while substantial savings remain.
Future replay may label outcomes, never become a feature or choose a per-event trigger by its
realized savings. - Report remaining savings and discriminator performance against decision delay, including
waiting, movement and recovery costs. For the tested actions and discriminator, a useful
interval exists only where both tests pass. Report no such interval if the signal arrives after
the residents or most of the benefit are lost.
Do not assume savings decline monotonically, select the best noisy time as recoverable regret,
or add overlapping event gains. An unexercised or underpowered interval remains inconclusive.
Keep true abstentions separate from missing opportunities: a false signal at an observed eligible
decision is exact baseline continuation, while an absent episode or eligibility window supplies
no action label. Do not require an entire control group's mean to be negative before recognizing
an individually paired harmful action that the signal rejects. Report both group gates and
individual losses. A failed earliest-trigger rule also does not exhaust the registered later
points; a timing hypothesis chosen from discovery needs its own frozen held-out confirmation.
Price the smallest sufficient history. A conjunction of episode-relative support tests can retain
the preceding sample's boolean verdict instead of all preceding measurements, provided it is
updated on every complete sample and reseeded when the reference changes. Keep the current-only
action predicate out of that history. A sampled clock may reproduce a request-indexed diagnostic
on a fixed unit-weight panel without doing so under buffering, weights or lifecycle changes;
verify the equivalence on the actual targets and state that boundary.
Only a surviving discriminator justifies an end-to-end candidate. Freeze its first-trigger and
subsequent behavior, then replay that candidate on its own evolving states: baseline event
tables cannot estimate the reachability or gain after earlier interventions. The existing
behavioral, corpus, holdout, invariant and runtime-cost gates still apply. A useful one-decision
signal justifies measuring that candidate; it does not establish the repeated policy's gain or
make unmeasured runtime cost either free or an obstruction.
.Claude Docs Jsr107 Conformance (`.claude/docs/jsr107-conformance.md`)
JSR-107 (JCache) Conformance Reference
Sources, audit methods, and standing rulings for the jcache/ adapter. Read the relevant topic
before raising a finding; these conclusions preserve accepted behavior and resolved defects.
Provider comparisons are recorded evidence, not a substitute for checking current source.
Sources and method
Use the JSR-107 1.1.1 specification and API javadoc. The 1.0 PDF predates changes togetCacheNames iterator behavior, typed getCache(String), and iteration over expired entries.
Compare the 1.1.1 API javadoc before relying on older text; the specification's revision history
records no 1.1 behavior change.
- JSR landing page
- Specification
- Local
cache-api-*-sources.jar:javax/cache/**javadoc and package documentation. - Reference implementation, version 1.1.1 sources:
https://repo1.maven.org/maven2/org/jsr107/ri/cache-ri-impl/1.1.1/cache-ri-impl-1.1.1-sources.jar.
Fetch the spec as needed; do not bundle it. Cite section or javadoc names because exports reflow.
SPEC_DOC=1ijduF_tmHvBaUS7VBBU2ZN8_eEBiFaXXg9OI0_ZxCrA
curl -fsSL "https://docs.google.com/document/d/$SPEC_DOC/export?format=txt" -o /tmp/jsr107_spec.txtFor an ambiguous behavior, read the spec, inspect what the TCK actually asserts, compare the RI
and at least three other providers, then check Caffeine's sibling paths. Explicit spec wording
outweighs RI behavior; the RI helps resolve ambiguity but has documented bugs. Preserve TCK
interoperability where its assertions are stricter than 1.1.1, and record the distinction.
Use the other providers to check whether the RI is an outlier. When providers split, favor the
spec text and RI interpretation and record the differing results.
Source comments and search summaries are not verified behavior. In particular, prior summaries
reversed Ehcache 3 and Coherence's synchronous listener exception handling.
| Area | Spec section / API | RI source |
|---|---|---|
| Expiry | Expiry Policies; ExpiryPolicy method table |
RICache, RICachedValue |
| Events and filters | Cache Entry Listeners; CacheEntryListener, javax.cache.event |
RICacheEventDispatcher, RICacheEntryEvent, RICacheEntryEventFilteringIterator |
| Counters and timing | Statistics Effects of Cache Operations; CacheStatisticsMXBean |
RICacheStatisticsMXBean, counter call sites in RICache |
| Loader and writer | Integration; CacheLoader, CacheWriter |
RICache.writeCacheEntry, deleteCacheEntry, batch calls |
| Entry processors | Entry Processors; MutableEntry, EntryProcessorResult |
EntryProcessorEntry, MutableEntryOperation |
| Copying | Store-By-Value and Store-By-Reference | RISerializingInternalConverter, RIReferenceInternalConverter |
| Configuration | Configuration; factories and type enforcement | RICache, configuration classes |
| Lifecycle and JMX | Caching Providers; CacheManager/Cache close contracts | RICacheManager, RICachingProvider, RICacheMXBean |
The spec's Annotations section (CDI/Spring) is outside this adapter's scope. The audit skill's
A–N matrix covers the remaining surface. Expiry, events, statistics, integration, and processors
need event/statistic assertions: the TCK often checks only final contents. Do not infer that an
operation's expiry-event column of “No” forbids an independently timed expiry notification.
For statistics, check the operation table as well as broad counter definitions: a read-through
get is a miss, not a put; clear differs from removeAll; expiry has no dedicated MXBean counter.
Provider source map
Fetch individual files or shallow-clone small modules. Locate current paths before citing them;
Hazelcast, Infinispan, and Coherence are large enough that a full clone is usually unnecessary.
| Provider repository | JCache expiry entry point |
|---|---|
cache2k/cache2k |
TouchyJCacheAdapter, ExpiryPolicyAdapter.calculateExpiryTime, durationToTicks |
oracle/coherence |
coherence-jcache/.../localcache/LocalCache, JCacheEntryMetaInf.modified, LocalCacheValue.updateInternalValue |
ehcache/ehcache-jcache (Ehcache 2) |
JCache.setTimeTo |
ehcache/ehcache3 |
Eh107Cache, Eh107Expiry |
hazelcast/hazelcast |
AbstractCacheRecordStore.updateRecord |
infinispan/infinispan |
jcache/commons/.../AbstractJCache.put / replace |
Find other domains' writer, MXBean, listener, and copier classes in the same modules. Trace
delivery as well as publication: Coherence's UPDATED notification comes through a backing-map
listener, not an explicit event call in put. Distinguish local and partitioned variants.
Validation pattern
Run :jcache:test and :jcache:tckTest for conformance changes. The latter unpacks tests intojcache/build/tck/org/jsr107/tck/; its cache-tests-1.1.1-test-sources.jar is also in the Gradle
cache. Read CacheExpiryTest and CacheMBStatisticsBeanTest assertions, not just test names.CacheLoaderTest.shouldPropagateExceptionUsingGet and shouldPropagateExceptionUsingLoadAll
assert the CacheLoaderException wrapping 1.1.1 still requires;CacheMBStatisticsBeanTest.testIterateAndRemove pins iterator hit/removal accounting.
Use a parameterized parity test when sibling operations should agree on events, counters, and
contents. Examples: JCacheCreationExpiryTest.writeOp_absent_zeroCreationExpiry andJCacheUpdateExpiryTest.writeOp_present_zeroUpdateExpiry. Establish the expected direction from
the contract first; an existing sibling can itself be wrong. Add a pin only where siblings diverge
or the TCK is silent on an outcome a caller or listener acts on; the TCK's per-operation checks
count as pins, and statistics vectors or event and processor tables for behavior recorded here are
not added for completeness.
Expiry
Zero expiry
Creation and update have different contracts. getExpiryForCreation() == Duration.ZERO means
the entry is not added; zero update expiry means it is updated and immediately expires.
| Case | Store | Operation event | Put count | Writer |
|---|---|---|---|---|
| Zero creation | Suppressed | No CREATED; EXPIRED carries the new value | None | Still runs |
| Zero update | Store immediately expired | UPDATED, then independently timed EXPIRED | Counted | Runs before commit |
Do not suppress the writer on zero creation. It persists the caller's write intent even though
the cache stores nothing, matching RICache.writeCacheEntry before creation-expiry evaluation.
Do not substitute the prior value or null in its EXPIRED event: that would hide disposal of the
new value from resource-tracking listeners. A read-through load under zero creation expiry follows
the same rule: get, getAll and a processor's getValue publish EXPIRED for the loaded value
and count no eviction. The RI publishes it from invoke and loadAll, but RICache.getValue
returns null silently for get and getAll.
Recorded creation-path comparison, from source on 2026-08-29:
| Provider | Writer before expiry guard | putIfAbsent result |
Put counted | EXPIRED |
|---|---|---|---|---|
| RI | yes | false | no | yes |
| Coherence | yes | false | no | no |
| Caffeine | yes | false | no | yes |
| Ehcache 2 | suppressed | true | unverified | unverified |
| Hazelcast | suppressed | unverified | no | no |
| Infinispan | below adapter; inconclusive | unverified | no | unverified |
| cache2k | delegated to core; inconclusive | unverified | unverified | unverified |
The RI and Coherence have comments suggesting suppression that disagree with their writer/event
call order; trace the implementation. CacheMBStatisticsBeanTest.testExpiryOnCreation pins zero
puts for put/putAll; CacheExpiryTest.expire_whenCreated discards putIfAbsent's result.
Caffeine records a miss for an absent zero-creation putIfAbsent, following the statistics
table's prior-presence rule and getAndPut; the RI incorrectly derives a hit from false.
Pin: JCacheCreationExpiryTest.putIfAbsent_absent_zeroCreationExpiry_recordsMissNotHit.
Recorded zero-update comparison:
| Behavior | UPDATED / put counted | Providers |
|---|---|---|
| Store immediately expired | yes / yes | RI, Coherence |
| Remove or suppress | no / no | Infinispan, Ehcache 2, Hazelcast, cache2k |
Measured cells: Ehcache 3.12.0 splits by path (put, invoke and loadAll(replace) store and
publish UPDATED then EXPIRED; getAndPut, putAll, both replace forms and getAndReplace
publish REMOVED for the old value); cache2k 2.6.1 publishes EXPIRED for the old value and counts no
put; Infinispan 16.2.3 publishes REMOVED and calls writer.delete. Hazelcast 5.7.0 hung on the
scenario, so its row keeps the recorded reading.
cache2k maps a zero update expiry to ExpiryTimeValues.NOW (ExpiryPolicyAdapter.durationToTicks),
and core's EntryAction.checkKeepOrRemove removes an entry expiring NOW.
Caffeine follows the first behavior because the spec distinguishes creation from update and the
RI agrees. All put/replace/invoke siblings gate suppression on creation only
(expirable == null). CacheExpiryTest.expire_whenModified checks only absence afterward, so
both behaviors pass. The parity tests above check the missing event/statistic distinction.
Policy defaults
- Creation returning null or throwing, a checked exception included, yields eternal expiry
(Long.MAX_VALUE), consistently inCacheProxyandJCacheLoaderAdapter. Null creation is
implementation-defined; the RI would NPE. The normal CREATED/put effects still occur. Pin for a
checked throw:CacheProxyTest.put_expiryThrowsCheckedException_storesWithDefaultExpiry. The
adapter catchesException, so anErrorpropagates; RI, Ehcache 3, Infinispan, and Coherence
catchThrowablearound expiry calls, Hazelcast catchesException, and cache2k does not catch.
Each write consults the policy before it publishes the attempted write's event, counts its put
or, in the put family, publishes an expired prior's EXPIRED, so theErrorleaves no event for
the aborted write, the order Hazelcast and cache2k use.invokeinstead publishes an expired
prior's EXPIRED before its processor and commits that removal if anything throws (see
entry processors). Pins:JCacheCombinedExpiryTest.*_expired_creationExpiryError_expiresOnceandJCacheUpdateExpiryTest.writeOp_present_updateExpiryError_publishesNoUpdate. - Update/access returning null or throwing leaves expiry unchanged. The loader helper takes a
createdflag; reload translates the updateLong.MIN_VALUEsentinel to the old wrapper's
deadline. Do not turn a finite deadline eternal or log an NPE for an ordinary null update.
Pins:CacheLoaderTest.reload_nullUpdateExpiry_keepsExpirationandreload_updateExpiryFailure_keepsExpiration. - Expiration counts in
CacheEvictions, whether discovered lazily or through native EXPIRED
removal. RI, Ehcache 3, cache2k, and Hazelcast exclude it, but the spec is ambiguous and offers
no expiry counter. This accepted choice makes expiry visible to dashboards; do not remove it
for ecosystem parity. - An expired entry found by remove/getAndRemove/removeAll emits EXPIRED and an eviction, not
REMOVED and a removal, andcontainsKeyreaps one the same way where the RI only tests the
deadline. The RI set overload disagrees with its own single-key/no-argument paths.
TCK pins:CacheExpiryTest.testCacheStatisticsRemoveAllandtestCacheStatisticsRemoveAllNoneExpired; the expired set-overload pairing is pinned byJCacheCombinedExpiryTest.removeAll_setOverload_expired.
Deadline conversion
ExpirableToExpiry subtracts in milliseconds, then converts the remaining duration once.
The wrapper deadline and ticker share an arbitrary origin. Converting the absolute deadline to
nanoseconds first saturates near the signed-long horizon: at Long.MAX_VALUE - 100, a one-minute
deadline formerly became 100 ns. The lazy Expirable.hasExpired gate was correct; the native
timer was the affected mirror. Custom tickers can reach this boundary, andJCacheFixture.START_TIME exercises the full long range.
Keep millisecond granularity (the native timer may fire up to about 1 ms later), core'sMAXIMUM_EXPIRY clamp, and call-site Math.max(0L, ...) handling of expire-now. Keep the ticker
read rather than using core's currentTime: dropping it was reverted because auto-increment
tickers shift observed times. Pin: JCacheExpiryTest.nativeDeadline_nearNanosecondSaturation.
Loader creation expiry uses the same ±1 correction as write/access expiry when a finite deadline
collides with sentinel 0 or Long.MAX_VALUE. Pins:CacheLoaderTest.load_adjustedTimeSentinelZero / load_adjustedTimeSentinelMax.
The millisecond deadlines and their sentinels (0 expire-now, Long.MIN_VALUE unchanged) assume a
clock that neither reads below zero nor crosses the signed wrap, and durations far belowLong.MAX_VALUE milliseconds. The eternal deadline is the same boundary: at a clock below zero an
unguarded hasExpired wraps it into expired, so an eternal replacement or an in-place renewal to
eternal seen by removeExpired's re-check is removed. These boundaries are accepted without a fix.
The recorded witnesses used custom boundary tickers; System.nanoTime documents that its values may
be negative, so the system ticker can also read below zero, at a frequency that is unmeasured.
JCache cannot express a lazy expiry below a millisecond: Duration rejects smaller units, andTypesafeConfigurator reads a configured duration in milliseconds, so a smaller one becomesDuration.ZERO.
Access expiry
getAccessExpireTime evaluates the policy; adjustAccessExpireTime writes the resulting deadline
to the held wrapper. Only lock-free reads call setVariableExpiration to update the
native timer by key, after writing the wrapper: any core read re-derives the native deadline from
the wrapper through ExpirableToExpiry, so one landing between the two writes would otherwise
restore the older deadline. Pin: JCacheAccessExpiryTest.get_concurrentRead_extendsNativeExpiration.
Writes already refresh the native timer through core's expireAfterUpdate, including a
same-wrapper return. Calling policy setExpiresAfter inside their compute violates the policy
API's atomic-scope restriction and can enter maintenance while holding a bin lock.
Read-path anchors: getAndFilterExpiredEntries, EntryIterator.hasNext, andLoadingCacheProxy.getOrLoad.
An access whose millisecond deadline equals the wrapper's writes neither the wrapper nor the native
timer. getExpiryForAccess() is still called once per access, which the TCK counts, andDuration.getAdjustedTime is millisecond arithmetic, so the skipped writes are unobservable.
Rewriting them made every hit take the node's lock and queue a write-buffer task throughsetExpiresAfter. Under AccessedExpiryPolicy on 8 threads a hit cost ~2.2 us, and ~0.2 us with
the skip. Pin: JCacheAccessExpiryTest.get_sameMillisecond_keepsNativeExpiration.
A replacement between a read and its by-key timer update can receive the old read's native deadline.
The old wrapper alone receives the timestamp, so this does not permit a stale value through its
lazy-expiry check, and a replacement that keeps the old deadline loses that read's extension, under
the standard AccessedExpiryPolicy too. getExpiryForAccess() is parameterless; standard
Accessed/Touched policies use the same access and creation duration. A custom access duration
shorter than creation can cause early native eviction and EXPIRED for the replacement. This race is
accepted; do not add a bin lock or identity guard to every access-expiry read.
get and getAll are happen-before, so a read whose captured wrapper has expired removes it only
by identity and continues with a live replacement that the removal kept: a key replaced before the
captured deadline was never absent. The removal also rechecks the captured wrapper's deadline at
the read's clock, so a wrapper that another read extended meanwhile is kept as well, containsKey's
removal included. LoadingCacheProxy.getOrLoad reaches the same answer by looking the key up again.
That lookup can find a concurrent insertion or replacement and return it without anotherExpiryPolicy.getExpiryForAccess() call, as can getAll's lookup of initially missing keys.
Native expiry still applies; ExpirableToExpiry mirrors the wrapper's stored creation/update
deadline. Skipping this one access adjustment is an accepted read-through race. Do not add a
special expired-wrapper recovery fix for the same miss-then-insertion pattern. Pinned byCacheProxyTest.getLoading_replacedBeforeExpiry_skipsAccessExpiry, withget_replacedBeforeExpiry_appliesAccessExpiry as the plain-read control.containsKey and EntryIterator.hasNext are last-value and may still skip it. A read whose thread
stalls across its own deadline between the two writes still loses its extension, becausesetExpiresAfter refuses an expired node; closing that needs the per-read key lock declined above.
The reverse ordering is the same accepted race from the other side: removeExpired'scomputeIfPresent re-reads the wrapper's volatile deadline at removal time, so a renewal already
written before that check runs is respected and the removal backs off. But if the check has
already passed and unlinked the wrapper before the renewal's write lands, that write reaches an
object no longer reachable from the map — a lost update, not a stale read, since the renewingget already returned its value at the linearizable point before the removal. Closing this needs
the same declined per-read key lock.
Pinned by CacheProxyTest.get_replacedBeforeExpiry, getAll_replacedBeforeExpiry,containsKey_extendedBeforeExpiry, get_extendedBeforeExpiry, and getAll_extendedBeforeExpiry.
An accepted timestamp-race report combined zero access expiry, an eternal entry, and a concurrent
processor READ to produce EntryProcessorException. Its postProcess path predates pre-processor
expiry reconciliation and is not a current reproducer; the lock-free boundary remains intentional.
The iterator stamps access expiry in hasNext() when staging the entry. Deferring to next()
widens its expiry hole or makes hasNext() promise an unavailable entry; the iterator contract
already allows next() to return null after expiry. Calling hasNext() without next() extends
one entry's deadline, an accepted cost. TCK: CacheExpiryTest.iteratorNextShouldCallGetExpiryForAccessedEntry.
CacheProxy.get always takes a fresh ticker.read() to derive the clock it checks hasExpired
against. LoadingCacheProxy.getOrLoad instead reuses the statistics start timestamp
(nanosToMillis((start == 0L) ? ticker.read() : start)) when statistics are enabled, so enabling
statistics changes which reading its expiry check uses while get's is unaffected. This is
intentional-enough, not a bug: the reused stamp is taken earlier in the method, so it can only
make the check see an older clock, which can only judge an entry live that a fresh read might
have judged expired — expiration is a maximum lifetime, and both readings satisfy that. Thecompute-based writes, remove(K,V) and replace(K,V,V) included, read the clock inside the
remapping function instead. A stamp taken before the compute is older by the wait for the entry's
lock, so it would judge an entry that expired during the wait as live and date a mismatch's access
deadline from before the wait. Pinned by JCacheAccessExpiryTest.removeConditionally_mismatchAfterLockWait
and replaceConditionally_mismatchAfterLockWait.
Native extensions
Returning the same wrapper from a query still reaches core's native-write metadata. FailedputIfAbsent (including keep-existing loadAll), a NONE-action processor, and eternal-policy
failed conditional writes/processor READ can reset native write time and cancel refresh. Thus
polling may defer vendor refresh.after-write or eager native TTL. This is accepted: JCache
expiry comes from ExpiryPolicy, while those Caffeine settings are optional extensions.
containsKey probes with getIfPresentQuietly, as keep-existing loadAll does. A recording read
would start a due vendor refresh, which calls the CacheLoader that the spec says a read-throughcontainsKey never calls, and it would count as an access, which the ExpiryPolicy table does
not give containsKey. Pin: CacheLoaderTest.containsKey_refreshAfterWrite_doesNotCallLoader.
With a vendor Expiry beside a JCache ExpiryPolicy, the native timer is not a mirror of the
policy, so its deadline after an access depends on the operation: a read sets it to the JCache
access duration, a compute-based access (invoke READ, a failed conditional, putIfAbsent on a
present key) leaves the vendor's expireAfterUpdate, and containsKey leaves it unchanged. Every
read still checks the wrapper's deadline, so only when eager eviction and its EXPIRED happen varies.
Do not expose RemapHints as a public no-op escape. Treating an unchanged value as no write
would change asMap().compute for all users and leave write-deque reorder paired with a stale
timestamp. A computeIfAbsent plus expired fallback splits the atomic operation and does not
solve invoke. These repairs were rejected.
A throwing extension Weigher (setWeigherFactory / setMaximumWeight) or native Expiry
(setExpiryFactory / ExpiryAdapter) runs in core after the adapter's remapping
function has written through and published the event, so it can abort storage after those
effects. Standard ExpiryPolicy is different: the adapter catches every Exception it throws
and uses the default duration, while an Error propagates.
The extension case remains accepted misuse of callbacks whose core contract forbids throwing.
Notifications/writer effects cannot be rolled back, and core has no post-metadata hook. Attempted
creation/update notifications are accepted; store/cache discrepancies need external reconciliation.
Do not move publication outside compute, which would lose per-key order. Unlike invoke,
put/getAndPut do not blanket-clear synchronous futures after arbitrary extension failure; a
pending notification reaching the next operation is also within this accepted boundary. A gate
must still be released from finally so dispatch can drain.
Entry processors
EntryProcessorEntry.Action records the dominant operation. Preserve these distinctions:
| Sequence / terminal action | Resulting effects |
|---|---|
Read-through getValue() / LOADED |
Miss, no put; follows Cache.get and the RI LOAD case |
| Remove an absent or expired entry / DELETED | Writer delete if write-through; no REMOVED or removal count |
| Load, then remove / LOADED → DELETED | Writer delete; intentionally differs from RI's cancellation |
| Create, then remove / CREATED → NONE | Full no-op |
| Load, set a value, then remove / LOADED → CREATED → NONE | Full no-op, including writer and events |
| Update a present entry / UPDATED | Writer must succeed before put count, UPDATED, and store |
The RI counts/fires a removal with null for absent processor removal, unlike its own remove(K).
Caffeine follows the listener table's requirement that an entry was removed; the statistics
table's wording is inconsistent. TCK covers remove-on-present only. For a read-through load,
pin the miss/no-put behavior with CacheProxyTest.invoke_readThroughLoad_recordsMissNotPut.
A remove that undoes this invocation's own create returns the entry to its untouched state: a
later getValue reads through if the loader has not yet been called in this invocation (as in
the RI), and a remove issued before that create is not written through (as in every provider
compared). A remove of any other state is DELETED and blocks read-through.
This terminal-action model follows the RI and the compared providers rather than the
specification's Entry Processors examples, which read more broadly. For expiry the
specification's own ExpiryPolicy table is explicit and takes precedence over Example 1's
first-read wording, so a read-modify-write invoke under AccessedExpiryPolicy keeps its
creation schedule. Example 3's writer delete after load, set and remove is the row above's full
no-op, which the RI shares and the TCK does not test.
invoke reconciles a lazily expired prior before the processor: publish EXPIRED, count eviction,
and expose absence. (A prior that core has already expired natively is instead reaped by core
with the quiet EXPIRED of dispatch and commit, which the caller does not
await.) If the processor or writer then throws any Throwable, commit removal of
that expired prior, await its synchronous listener, and rethrow after compute viaprocessorFailure (Error unchanged; other failures as EntryProcessorException), with a
listener failure suppressed onto either. Otherwise the already-published expiry would be orphaned
and could fire again. postProcess is expiry-free; READ/UPDATED require a live prior, so do not
re-read the clock or restore an expiry check there. Pins:EventDispatcherTest.invoke_expiredAndProcessorThrows_retainsListenerFailure andinvoke_expiredAndProcessorThrowsError_retainsListenerFailure.
Read-through MutableEntry.getValue() normalizes the direct loader call before returning control
to the processor: existing CacheLoaderException passes through, other Exceptions are wrapped
in it, and a direct InterruptedException first restores interruption. Error propagates unchanged.
A processor can therefore catch the loader failure and store a fallback. Keep this guard local;
the storage-oriented loader adapter would duplicate copying, expiry and event publication.
Pins: CacheLoaderTest.invoke_loaderFailure_canRecover,invoke_loaderInterrupted_restoresInterruptStatus, and invoke_loaderError_propagates.
Exceptions escaping processor invocation, including write-through failures, surface asEntryProcessorException; the spec's “Exceptions in EntryProcessors” section includes failures
from the caching implementation itself. Single-key writer failure therefore surfaces asEntryProcessorException(CacheWriterException). The RI exposes a raw writer exception because
its writer is outside the catch; cache2k wraps and Ehcache 3 exposes CacheWritingException.
TCK accepts all as CacheException; explicit spec text governs this corner.
Pre-processor copy failures in single-key invoke, of the key or the prior value, remain a rawCacheException, consistent with the API's general cache-failure surface: invocation begins
when the processor is called. invokeAll, however, must isolate each key's
runtime failure in its EntryProcessorResult: preserve an existing EPE, wrap a non-EPE once,
and continue. Otherwise one uncopyable key aborts the batch and discards completed results.
RI, Ehcache 3, cache2k, and Hazelcast also isolate per-key failures (some double-wrap EPE).
Pin: CacheProxyTest.invokeAll_perKeyFailure_isolatedNotAborted.
Both invoke variants forward an explicitly null varargs array unchanged; only the key/keys and
processor require null rejection. RI, Ehcache 3, cache2k, Hazelcast, and Infinispan forward null;
Coherence indirectly reads its length. Normal omitted varargs remain an empty nonnull array.
Pins: CacheProxyTest.invoke_nullArgumentsArray_forwarded andinvokeAll_nullArgumentsArray_forwarded.
getValue on a DELETED entry returns null without loading. A successful null read-through load
is consumed rather than retried by another getValue in the same invocation. A failed load
leaves the loader available for a processor that catches the failure and retries. Keep loader
consumption and LOADED assignment after successful return, not in a finally block. Pins:CacheLoaderTest.invoke_removedEntry_doesNotLoad, invoke_nullLoad_consumed, andinvoke_loaderFailure_canRetry, each covering invoke and invokeAll.
Read-through and event ordering
Read-through getAll materializes loaded values with core put, replacing a value written
concurrently during the load, and JCacheLoaderAdapter.loadAll publishes CREATED for the loaded
value. This accepted choice treats the load as a fresh read of the system of record and preserves
notification for resource-tracking listeners. Do not align it withloadAll(replaceExistingValues=false), whose explicit contract is to keep existing values. That
separate API uses loadAllAndKeepExisting / putIfAbsent. Under replaceExistingValues=true a
present key the loader cannot load is kept by the RI, Caffeine, Hazelcast and Infinispan and removed
with a REMOVED event by Ehcache 3.12. The spec's Read-Through table says getAll invokesloadAll(), which Caffeine and Ehcache 3 follow (Ehcache once per missing key), while the RI,
cache2k 2.6 and Hazelcast 5.7 load per key through load; a loader whose loadAll throws (for
example UnsupportedOperationException) therefore works with the RI's getAll and fails Caffeine's
with CacheLoaderException, and only Caffeine's getAll sees the bulk shapes. Two CREATED events
without an intervening UPDATED are an accepted concurrent-load outcome. A key the loader returns
outside the request is unspecified by CacheLoader.loadAll and the TCK, so it is stored and
published as CREATED like any loaded value, even over an existing mapping; a presence check before
core's put would be racy check-then-act. The same side-load does not classify the prior: it
replaces an unrequested key's JCache-expired prior without its EXPIRED event or eviction when native
expiry does not mirror the JCache deadline (a vendor expireAfterWrite), and with the default
mirror the EXPIRED arrives after the CREATED; under zero creation expiry the unrequested key's
EXPIRED names the discarded load while the prior stays live. loadAll routes such keys through the
write path and announces the expired prior first, as the RI does.
The overwrite holds under write-through and for a concurrent delete as well. A put or remove that
commits through the writer while the bulk load runs is undone when the loaded value is stored: after
a put the store holds the new value and the cache the loaded one, and after a remove the deleted
row is back in the cache. Either way the cache trails its store until the entry expires. This is
accepted. The spec gives getAll per-key happen-before but also has it invoke loadAll(), and no
surveyed provider reconciles the two with a batched load: the RI and Ehcache keep the write by
loading one key at a time under that key's lock, Ehcache through a single-key loadAll. A per-load
token that every write discards would keep both the batch and the write, at the cost of a hook on
each adapter write path to make the adapter stricter than any provider; core's own bulk overwrite
stays as its contract states. Cache.loadAll has the same shape, and its consistency row is "N/A".
An application that needs the write kept can use Caffeine's AsyncCache.getAll, whose bulk load does
not stomp a write to a requested key.
The listener contract does promise per-key event ordering for synchronous and asynchronous
listeners, and says listeners fire after the cache mutation. The accepted read-through behavior
diverges from those promises: loader publication precedes core bulk storage, so CREATED can
precede EXPIRED for an expired prior even without a concurrent writer. Single-key loading first
discards the expired entry. Do not describe the contract itself as lacking an ordering promise.
Recorded evidence: the inversion occurs when the reload lands before the timer wheel has swept
the prior's bucket, whichever executor runs maintenance. On 2026-09-24 a getAll about 100 ms
after a 50 ms deadline inverted in 17 of 20 runs on the direct executor and 16 of 20 on the
common pool, and in none at about 1.25 s. The sweep follows a level-0 timer-wheel tick (2^30 ns of
the ticker): on the direct executor the inversion occurs exactly when no tick has passed between the
prior's write and the getAll, and on the common pool a tick lets the sweep win only usually, more
rarely under CPU contention. The transitions can also end at a value the cache does not hold: when
a concurrent put commits between a bulk load's CREATED and its insert, the load overwrites the
put's value, so a mirroring listener's last event names a value the cache no longer holds and no
later event corrects it. That is the accepted bulk overwrite's event face (see the write-through
paragraph above). The accepted rationale is
the timing/transport behavior of distributed providers, while preserving the contract difference:
Hazelcast 5.7's per-entry cache-event factories did not set the publication order key (bulk
removal used the key-set hash); Coherence localcache dispatched EXPIRED before loading, but its
partitioned expiry used a separate synthetic-event listener and delivered asynchronously even to
synchronous registrations. The RI accumulates per-operation events in one JVM and orders this
case. Arrival order in a distributed replica also cannot cover lost-node or split-brain events;
the recorded decision accepts reconciliation by transition rather than imposing a new adapter
ordering guarantee. These observations are not a claim that the spec permits the divergence.
If a concrete requirement justifies repair, the recorded direction is core getAll: discard
expired entries in the miss set before loading, as single-key loading does. The adapter's quiet
accessors expose no expired mapping for a conditional removal. Treat publication-before-storage
and reload-before-prior-expiry as the same accepted design choice, not independent new findings.
Statistics
Operation timing
- Loading get/getAll opens a per-thread timing scope after recording a miss.
JCacheLoaderAdaptercontributes only time spent indelegate.load/loadAll; the outer
operation records total minus loader time once, fromfinally. Copying, expiry evaluation,
publication, and failure handling stay included. Native loads throughunwrap(LoadingCache.class)have no scope and do not alter JCache get time. - Do not restore a negative global pre-credit. An output-copy failure or native load without
an enclosing JCache read left that credit unbalanced and madeAverageGetTimenegative.
RI, Hazelcast, Infinispan, and cache2k avoid negative means, although some include loader time.
Pins:CacheLoaderTest.load_outputCopyFailure_keepsGetTimeNonNegative,load_failure_excludesLoaderTime,load_copierTime_includedInGetTime, andnativeLoad_doesNotChangeJCacheGetTime(single and bulk variants). - Successful plain getAll ends its timer after
copyMap, matching get and loading reads.
RI and Coherence include return conversion; Hazelcast's bulk per-key timer and cache2k's
core load-time metric differ. The MXBean's execution-time definition and internal parity
support inclusion. Pin:CacheProxyTest.readOp_outputCopyTime_includedInGetTime.
Failed-read hit accounting remains distinct: plain get and the RI count a hit only after
successful output conversion. - PutAll starts timing before preparing
CopiedEntryinputs; all copies still finish before
writeAll and only successful stores count as puts. RI starts before conversion; Coherence
delegates to its copy-inclusive put, while Hazelcast times below serialization and cache2k
reports no put mean. Pin:CacheProxyTest.writeOp_inputCopyTime_includedInPutTimeacross
put/putAll, create/update, and statistics enabled/disabled. CacheGets = hits + misses. Caffeine divides each average duration by its own operation
counter; the RI's three averages all divide by gets, a known RI bug to avoid copying.recordHits/recordMissesand the conditional pairs that call them (replace(K,V,V),
batch hit/miss splits) are unconditionalLongAdder.add, unlikerecordPuts/recordGetTime/recordPutTime/recordRemoveTime, which also gate on a nonzero count. The asymmetry is
accepted under best-effort statistics: the zero count costs one uncontendedLongAdder
update, and rewriting the pairs asif (found) recordHits else recordMisses, as the sibling
call sites read, was declined as churn.invoke, conditionalremove, and iterator yields count gets without timing them, and an
emptygetAlltimes no gets. The RI times the first three and not the last; these populations
stay unaligned under best-effort statistics.putIfAbsent's hit or miss is untimed too, as in
the RI. The same mismatch reachesinvoke's put and
removal counters:postProcesscallsrecordPuts/recordRemovalsfor CREATED/LOADED/UPDATED/
REMOVED actions but never opens a timing scope, sogetAveragePutTimeandgetAverageRemoveTimeare diluted byinvoketraffic the same waygetAverageGetTimeis.
Commit and failure accounting
Writer failure suppresses the operation's put/removal count as well as its mutation event and
store. This includes invoke UPDATED, whose writer must precede the count. Parity pins:CacheWriterTest.writeOp_failingWriter_noPutsRecorded, writeOp_writerSucceeds_recordsPut, andremoveOp_failingWriter_noRemovalsRecorded across direct, batch, processor, and iterator paths.
After a committed effect, capture a synchronous listener failure with awaitSynchronousFailure,
record the required counters/timers using the operation's existing start-state rules, and thenrethrowListenerFailure. Direct operations follow bulk/invoke/loading siblings. Returned-value
copying in getAndRemove/getAndReplace happens after accounting, as in getAndPut, so copier failure
does not erase the committed effect. The spec does not prescribe this ordering against listener
failure; required counters and sibling parity justify it. This covers the specified listener
exception, not arbitrary Error; enable/disable races remain spec-undefined. When a synchronous
listener throws an Error, awaitSynchronousFailure does not capture it, so the statistics block
is skipped: the committed mutation is retained but its counters are not recorded for that
operation. The mutation is not rolled back. Pins:EventDispatcherTest.synchronousListenerFailure_committedMutationRetainsStatistics andsynchronousExpiredListenerFailure_retainsGetStatistics.
Write-through
Single-key writer calls and their cache mutations share the per-key compute lock. This followsCacheWriter's non-batch atomicity contract. In particular remove/getAndRemove must use compute,
not computeIfPresent, because delete must run even for absence. A writer call before compute lets
a same-key put interleave, leaving cache and store inconsistent. Preserve publishToWriter so
bulk loops can reuse the mutation without repeating writer calls. Pin:CacheWriterTest.removeThrough_racingSameKeyPut_noStoreCacheDivergence.
RI, Ehcache 3, Hazelcast, cache2k, and Coherence partitioned serialize these effects; recorded
Coherence localcache had the old unlocked writer/removal window.
Bulk writeAll/deleteAll runs once before per-key computations. The spec exempts batch methods
from cross-key atomicity; a racing single-key operation can invert cache/store order across
that window. This is accepted for both putAll and removeAll. RI locks all keys beyond the spec;
Ehcache 3 and Coherence partitioned batch like Caffeine, while cache2k/Infinispan/Coherence local
loop per-key through their persistence SPIs. Do not replace batching with per-key writer calls
solely for RI parity. The Integration contract also bounds write-through guarantees to the cache
being the application's only writer to the external resource.
Treat a non-throwing writeAll (putAll) or deleteAll (removeAll) as full success. Honor a
writer's residual collection only on a partial failure, when it identifies entries that failed.
The RI's residual-on-success interpretation made both putAll and removeAll a no-op for a
non-clearing writer and was rejected for both. Pins:CacheWriterTest.removeAll_nonClearingWriter_stillEmptiesCache andputAll_nonClearingWriter_stillStores.
A partial batch failure reconciles the same way whatever Exception the writer throws, including
a checked exception thrown undeclared by another JVM language. A writer Error after a partialwriteAll/deleteAll propagates before the store loop, leaving the external resource changed
for the completed entries and the cache unchanged, as in the RI. Pins:CacheWriterTest.putAll_partialSuccess_storesWrittenEntriesOnly andremoveAll_partialSuccess_removesDeletedEntriesOnly. The specification requires wrapping anyException a writer or loader throws. Recorded from bytecode on 2026-09-14: RI, Ehcache 3,
Hazelcast, Infinispan, and cache2k catch Exception or Throwable around loader and writer calls
(Ehcache and cache2k in their cores), while Coherence's writeAll and deleteAll catch onlyRuntimeException.
The RI swallows a CacheWriterException thrown by writeAll/deleteAll, leaving nothing to
rethrow; Caffeine propagates it, as CacheWriter.writeAll's "if thrown cache mutations will occur
for entries that succeeded" requires. Pins:CacheWriterTest.putAll_fails_propagatesSameWriterException andremoveAll_fails_propagatesSameWriterException. When a write-through put over a JCache-expired
prior fails, the RI has already removed the prior and skips its EXPIRED, and a read-through get orgetAll whose loader throws over an expired prior removes it the same way; Caffeine keeps the prior
until a later read or commits and announces it, one EXPIRED either way.
Converting or defaulting a callback's InterruptedException first restores the thread's interrupt
status, as core's Caffeine.toUnchecked does, since the wrapper or the default would otherwise hide
the interruption. Pins: CacheLoaderTest.readThrough_loaderInterrupted_restoresInterruptStatus,CacheWriterTest.writeOp_writerInterrupted_restoresInterruptStatus, andCacheProxyTest.put_expiryInterrupted_restoresInterruptStatus.
The batch exemption does not permit writes the adapter later refuses because copying failed.
Copy all putAll keys/values before writeAll and reuse CopiedEntry in the store loop. A copier
failure then writes/caches nothing. Pins: CacheProxyTest.putAll_writeThrough_copierThrows_doesNotWriteToTheStore
and EventDispatcherTest.putAll_copierFails_abortsBeforeAnyCommit. If a throwing native extension
still aborts a loop after partial writer failure, preserve that CacheWriterException as
suppressed, not replaced. Pin: CacheProxyTest.putAll_writerPartiallyFails_storeThrows_retainsWriterFailure.
No-argument removeAll delegates to the native map key set, which filters natively expired entries.
Those expired-but-unreaped keys are omitted from deleteAll, unlike the RI. This remains an
accepted reading of an existing mapping, with no TCK assertion resolving it. The opposite half of
the same method has the opposite outcome: a key that is JCache-expired but still natively live is
present in that key set, so it is still passed to writer.deleteAll before being reaped as EXPIRED
plus an eviction — not REMOVED. CacheWriter.deleteAll allows both halves, since under 1.1.1 its
keys "may include keys even if there is no mapping". What the second half departs from isCache.removeAll()'s "If the cache is empty, the CacheWriter is not called" when every resident is
JCache-expired, which needs a vendor native expiry that outlives the JCache deadline. Both halves
are recorded together here rather than only the natively-expired half.
Events and callbacks
Dispatch and commit
Append events inside the mutating compute to preserve per-key ordering, but stage listener
execution until it returns. beginComputation marks the publishing thread; the first publish
lazily creates a gate. endComputation releases the gate from finally, holding the mark through
release because a caller-runs executor may dispatch there. A throwing Weigher/native Expiry must
not leave the chain blocked. No listeners means no gate allocation, and the release clears the
gate's slot with set(null): remove() discarded the thread-local entry, so every computation
allocated a fresh one. A drain likewise empties the thread's pending list in place, except one
holding more than MAX_RETAINED_PENDING futures, which it releases: a bulk operation with a
synchronous listener otherwise left each thread holding its largest array until the thread died
(about 4.9 MB after a 1M-entry removeAll). Pins:EventDispatcherTest.awaitSynchronous_largeBatch_releasesPendingList andignoreSynchronous_largeBatch_releasesPendingList. Do not stage an event outside a computation,
such as an unwrap-driven load, because it has no matching release.
This staging makes ordinary listeners observe committed mappings. RI dispatches after put under
its key lock; Ehcache 3 releases a StoreEventSink after compute; Hazelcast publishes after record
mutation; Infinispan forwards post events; Coherence uses post-mutation MapEvents. Recorded cache2k
listeners run before its final value write. The read-through publication exception is described
above. Pins: EventDispatcherTest.publish_listenerObservesTheCommittedMutation (a created
listener, observing through the native cache because callback JCache re-entry is refused),publish_computationThrowsAfterPublishing_doesNotWedgeTheKey, andCacheProxyTest.unwrap_nativeLoad_dispatchesAndDoesNotStallTheKey.
Every publication uses a dependent CompletableFuture stage, including the first event for a key.
A source supplyAsync calls executor.execute synchronously; rejection inside the cache compute
can abort the cache mutation after the writer succeeds. A dependent stage captures rejection in
its future, so commit completes and the synchronous caller receives CacheEntryListenerException
afterward. This fixed the earlier first-event/successor difference under the non-batch writer
atomicity rule; the rejecting executor is still misconfigured. The “first” branch is common
because idle-key cleanup empties the slot. Pin:CacheProxyTest.put_writeThrough_eventExecutorRejects_doesNotSplitTheStore.
The queue needs no extra lock: append is an atomic map compute, cleanup is conditionalremove(key, future), and its preceding identity get is only a fast path. A successor installed
before cleanup survives; cleanup before a successor permits a new chain only after the old one
completed. Await/ignore pending synchronous futures on relevant exits and clear in finally.
Bulk operations must drain already-published events even when the loop fails. putAll copying
now happens before any commit, covered byEventDispatcherTest.putAll_copierFails_abortsBeforeAnyCommit; the earlier mid-loop copier test
was removed, but later failures still require draining.
invoke's outer catch discards rather than awaits the pending synchronous future
(ignoreSynchronous()) before rethrowing. That choice rests on every postProcess branch callingpublishToCacheWriter before its publish*, so a throwing writer aborts before anything is
published and there is nothing pending to leak. This ordering premise is not restated at the catch
site; a future edit that publishes before the writer would silently leak a synchronous future here
without a failing test, since the accepted native-extension and Error boundaries are a separate,
already-scoped exception to the same rule.
An accepted report described duplicate EXPIRED, but exactly-once eviction counts, when a listener
ran before executor rejection aborted a reap. It predates dependent-stage publication. Preserve
the accepted executor-failure boundary, but require a current throwing path before claiming that
mechanism remains reproducible.
Native size/weight eviction publishes quiet REMOVED; native EXPIRED publishes quiet EXPIRED.
Refresh reload publishes quiet UPDATED, EXPIRED for zero update expiry, or REMOVED on a miss.
The last two remove the entry but count neither an eviction nor a removal: core removes it as
an explicit removal, outside JCacheEvictionListener, and statistics are best-effort.
Quiet means no synchronous caller await: it informs resource-tracking listeners without blocking
the evicting/refresh thread. Lazy expiry mostly arrives this way on the system ticker: the native
mirror hides an expired entry from reads, and a read in the sub-millisecond gap between the
wrapper's millisecond deadline and the native one re-derives the native deadline from the expired
wrapper, so removeExpired finds the entry natively expired. A read, or a computeIfPresent-based
replace or conditional remove, that finds an entry expired therefore publishes and counts nothing,
and its EXPIRED and eviction wait for a maintenance cycle after a wheel tick, which an idle cache
without a Scheduler does not run. The adapter's own read-side reap runs only when a vendor
native expiry outlives the JCache deadline. For a read that finds an expired entry, the RI 1.1.1,
Ehcache 3.12.0 and Infinispan 16.2.3 deliver EXPIRED to a synchronous listener before the read
returns, cache2k 2.6.1 does not, and Hazelcast 5.7.0 does so only in some JVM histories, so the
deferral is a Caffeine choice (lazy expiry; a Scheduler for promptness) rather than ecosystem
parity.
Clearing natively expired residents can produce quiet EXPIRED and eviction counts through core's
removal cause, even though ordinary explicit clear removals are silent. Closed-cache delivery is
separately suppressed by dispatch's closed check. A reload publishes before core decides whether
it commits, and core reports a reload it discards because the entry changed in flight only to a
removal listener, which the adapter does not register. That event stands with no mutation, which
is accepted for a best-effort extension that has no post-commit hook. Native eviction and expiry
publish the same way, from core's eviction listener inside the eviction's compute on an unmarked
maintenance thread, so their dispatch is not staged either: a size-evicted entry's REMOVED listener
can read the mapping back before the removal commits, within microseconds of it, and on a
caller-runs executor the listener runs inside that compute, where writing its own key fails withConcurrentHashMap's Recursive update. An expired entry is hidden from reads, so EXPIRED shows
nothing stale. Accepted on the same terms: JSR-107 defines no eviction, and staging it would need
core's post-unmap removal callback to release a gate per eviction.
Listener failures
Filters run inside compute and decide whether an event is published. A filter failure, anError included, is logged and returns false from GuardedCacheEntryEventFilter.evaluate; it
must not abort a mutation after the writer or earlier registrations already took effect. This
differs from post-commit listener failure. RI commits before propagating filter failures;
Hazelcast/cache2k filter at delivery. Pin: EventDispatcherTest.publishCreated_filterThrows.
A synchronous listener's CacheEntryListenerException passes through; other listener runtime
exceptions are wrapped in it. javax.cache.event package-info and CacheEntryListener require
propagation. Listener Error is logged and rethrown unchanged, not wrapped. Ordinary listener
failure travels as a chain result so it does not break subsequent same-key delivery;awaitSynchronous throws the first failure and suppresses extras. Exceptional executor futures
also become CacheEntryListenerException. Async/quiet failures are logged because there is no
synchronous caller. Mutation remains committed. Spec, RI, cache2k, and recorded Infinispan sources
support propagation. Ehcache 3.12.0, Hazelcast 5.7.0's member event path, and the recorded Coherence
implementation log failures. Hazelcast's listener adaptor releases its completion latch in finally andStripedExecutor catches the failure; the waiting cache proxy has no listener-exception result.
Pins: EventDispatcherTest.put_syncListenerThrows_propagatesToCaller,publishCreated_asyncListenerThrows_swallowed,publishCreated_syncListenerThrows_subsequentEventStillDelivered,awaitSynchronous_listenerException, awaitSynchronous_listenerExceptions_suppressed, andEventTypeAwareListenerTest.
A listener failure must not re