Repository navigation
test(corpus): pair every detector, and fix what reading the refusals found - #1185
Merged
Merged
Conversation
…om peers' samples LIVELOCKS reported CRITICAL on healthy code. A probe on platform workers drew it from a body that only increments a counter: each worker sampled from its own finally saw itself RUNNABLE and its finished peers WAITING in the pool's queue, so a run that finished every round alternated between the two and read as rapid state cycling. On the default virtual workers the same samples were empty, and a real spin was not a finding on either, so the detector could say nothing true. The one observation that can show a livelock is that the body did not finish. The runner now marks workers in and out of the body and asks the detector to classify those still inside when a round times out, before it interrupts them. Thread.getState() answers for virtual threads too, so the runner.detector.inert event for this detector is gone, and the dumpAllThreads per body with it. The only threshold is the timeoutMs the test chose. captureSnapshot() stays for a body sampling peers. Verified: LivelockRoundTimeoutTest failed 4 of 4 before this change (a false CRITICAL on healthy bodies, silence on a spinning platform worker, a spinning virtual worker and a parked worker) and passes after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found reading the detectors the corpus refused to pair, to find the silent half each refusal said could not exist. DeprecatedThreadApiDetector documented its name as one of DEPRECATED_APIS and never read that list, so a wrapper recording every thread call reported Thread.interrupt at HIGH. Thread.stop, suspend and resume are gone in JDK 26, which CI builds on, so weaving them is not an option: a body that compiles everywhere can only record. VirtualThreadCpuBoundTaskDetector measured the segment after the last yield, so one yield just before the end excused everything before it, on a report that says "ran N ms without a yield point". AgentConcurrencyUtilHooks already wove obtrudeValue and obtrudeException for the happens-before model and told COMPLETABLE_FUTURE_OBTRUDE_ABUSE nothing, so it needed a hand-written record to see a call the agent had just substituted. It is agent-fed now, which is what makes complete() a real silent twin. Verified: both new unit tests failed before the change. The obtrude feed is held by the corpus pair in the next commit, whose firing row depends on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The corpus could not prove every detector works: 13 of 148 had no pair, each with a written reason, and seven more agent-fed detectors had only pairs that told them what happened (#1161, #1162, #1163). Read against their detectors, the refusals were out of date or named a silent half nobody had written, so the list is gone and a detector with no pair now fails EveryDetectorIsPairedTest. A row reading a clock fires by construction, because a slower machine only waits longer, and its twin does not read the clock at all where that is possible. Latches force the gap LOCK_DOWNGRADE and MEMORY_ORDERING were refused for needing. A slf4j-api BasicMDCAdapter behind a provider in test sources replaces slf4j-nop, so a leaked MDC key is visible with one dependency fewer, and a stub under the name the agent matches reaches the woven EntityManager path. Verified: all six lanes pass locally on JDK 26. Each row's outcome was read from its report. Closes #1161 Closes #1162 Closes #1163 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…does COMPLETABLE_FUTURE_OBTRUDE_ABUSE sees the woven call now, so ASSERTED understated it and DetectorEvidenceMatchesCodeTest refused the row. It is OBSERVED like EXPLICIT_GC. Both stay at PROMPT, held in PairEvidence with the reason: a recovery path and a WeakReference test make these calls on purpose and would draw the same finding, so a VERDICT would fail correct builds. virtualThread_yieldPointResetsTimer_noViolation pinned the defect fixed earlier in this branch: a 300 ms spin excused by one yield just before the end. It now holds what a yield is for, ten 30 ms segments against a 250 ms threshold. LivelockRoundTimeoutTest drives the engine, so it carries @e2e as E2eTagGuardTest requires; that means -P fast does not run it and -P e2e does. Verified: the fast tier passes across the reactor (3903, 97 and 13 tests) and the e2e selection ran LivelockRoundTimeoutTest, 4 of 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README, the catalog, the corpus write-up, the changelog and the livelock example described refusals that no longer exist and a LivelockDetector that sampled at the end of each body. A reader deciding whether to trust a finding read the wrong thing, and the example's README promised output it could not produce. The write-up keeps the earlier sections as the record of what was believed, and adds a table of what each refusal said against what reading the detector found, the limits of a pair where the silent half tests the weave rather than a judgement, and the one-off run that showed the rows can fail: with 20 detectors silenced all 20 new firing rows went red, and with the deciding condition removed from 12 detectors the 12 matching silent rows did. The EXPLICIT_GC twin and the seven woven per-thread twins were not measured that way, and it says so. Guardrails and diagrams are regenerated through tools/guardrail-drift.sh's path list and tools/generate-architecture-diagrams.sh, because the runner's load-bearing note and its sequence changed. Verified: CorpusClaimsInDocsTest passes on all six lanes, which derives every count these documents state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stuck round time to start CI found two things JDK 26 on Windows did not. Consumer Fixture and the seven JUnit legs recorded "Thread.getId()" and got no finding once the detector read DEPRECATED_APIS. Thread.getId is deprecated since JDK 19, so the list was incomplete as well as unread, and tightening it dropped a legitimate name without a word. It joins the list, the fixture uses the listed spelling, and the changelog tells a consumer passing another name what changes. Test Suite (21, ubuntu) failed aSpinningVirtualWorkerIsACandidate with an empty report, which means no worker was in its body at the 300 ms deadline. timeoutMs counts from the start of execute, before the run's detectors are built, so a loaded runner can spend it on setup. That cause fits the evidence and is not confirmed. Both the test and the corpus row now allow 2000 ms. Verified: LivelockRoundTimeoutTest, DeprecatedThreadApiDetectorTest and the consumer fixture (286 tests) pass locally. Corpus Eval had already passed on JDK 21 and 25 on the previous commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 3, 2026
Open
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The corpus could not prove every detector works. 13 of 148 had no pair, each with a written reason, and 7 agent-fed detectors had pairs that only told them what happened. Every detector now has a row that must fire and a twin that must stay silent, and a detector with no pair fails the build.
Reading the refusals against their detectors, instead of adding rows around them, found real defects.
Defects this found
LIVELOCKSreportedCRITICALon healthy code. A probe drew it on platform workers from a body that only increments a counter: each worker sampled itselfRUNNABLEin its ownfinallyand its finished peersWAITINGin the pool's queue, so a run that finished every round read as rapid state cycling. It saw nothing on the default virtual workers, and a real spin was a finding on neither. It now classifies the workers still in their bodies when a round times out, throughThread.getState(), on both kinds of worker.ConcurrencyRunnerchanged for this, and therunner.detector.inert detector=LivelockDetectorlog event is gone (a contract under invariant 8, so the test pinning it changed with it). So is adumpAllThreadsper body.DEPRECATED_THREAD_APIreported any name. It publishesDEPRECATED_APISand documents the argument as one of them, and never read the list. This is a behaviour change for a consumer passing a name off the list, which now draws no finding.VIRTUAL_THREAD_CPU_BOUNDmeasured only the segment after the last yield, so one yield before the end excused a computation of any length.COMPLETABLE_FUTURE_OBTRUDE_ABUSEneeded a hand-written record of a call the agent had just woven. It is agent-fed now.Decisions to check
virtualThread_yieldPointResetsTimer_noViolationwas rewritten, not satisfied. It pinned the yield defect above: a 300 ms spin excused by one yield. It now holds ten 30 ms segments against a 250 ms threshold.DEPRECATED_THREAD_APIis not woven.Thread.stop,suspendandresumeare gone in JDK 26 (checked withjavapon 21 and 26), so a body cannot make the call on every JDK this builds on. Its pair is told.EXPLICIT_GCand the obtrude detector stay atPROMPT, held inPairEvidencewith the reason: aWeakReferencetest and a recovery path make those calls on purpose.slf4j-nopis removed fromcorpus-eval. A provider in test sources usesslf4j-api's ownBasicMDCAdapter, so a leaked MDC key is visible with one dependency fewer.What a pair shows, and what it does not
EXPLICIT_GCandCOMPLETABLE_FUTURE_OBTRUDE_ABUSEreport every event they receive, so their twins hold the weave to selecting the right call rather than testing a judgement.PairEvidenceholds those belowVERDICT.corpus-eval.mdhas a table of what each of the 13 refusals said against what reading the detector found.How it was verified
LivelockRoundTimeoutTest4 of 4, plus the deprecated-name and yield testsNot verified: the
EXPLICIT_GCtwin and the 7 woven per-thread twins were not mutated, so nothing has shown those silent rows can fail (#1181). The full-P e2esuite and JDK 21 and 25 ran only in CI, not locally. The mutation runs were one-off and are not a standing gate.What CI found that local runs did not:
Consumer Fixtureand the 7 JUnit legs recorded"Thread.getId()", which the tightened list dropped.Thread.getIdis deprecated since JDK 19, so it joined the list, and the changelog's Upgrading section tells a consumer passing another name.Test Suite (21, ubuntu)failedaSpinningVirtualWorkerIsACandidatewith an empty report, so no worker was in its body at the 300 ms deadline.timeoutMscounts from before the detectors are built. That cause fits the evidence and is not confirmed; the test and the corpus row now allow 2000 ms.OS-Sensitive Tests (macos-latest)was cancelled once with no runner and no steps while githubstatus reported degraded service, and passed on re-run.Docs
README,
docs/DETECTOR_CATALOG.md,docs/detector-catalog/01-foundations.md,docs/analysis/corpus-eval.md,docs/analysis/corpus-eval-future-improvements.md,docs/agent/diagnostics.md,docs/architecture/runtime-guarantees.md,docs/DEPENDENCIES.md,docs/CHANGELOG.md,corpus-eval/README.md,examples/README.mdandexamples/07-livelock. Guardrails and diagrams are regenerated.VibeTags: considered
LivelockDetector.roundTimedOutand the new runner calls, and added nothing: the one fact worth keeping, that it runs before the interrupts, is a comment at the call. The existing@AILoadBearingnote onexecutenamed the removed snapshot and was corrected.Closes #1161
Closes #1162
Closes #1163
Follow-ups: #1179, #1180, #1181, #1182, #1183, #1184
🤖 Generated with Claude Code