Skip to content

Six of the last ten CI failures were unrelated single integration tests — a flake population taxing every second PR #1384

Closed

Description

@rbuergi

Six of the last ten failed CI runs across all branches failed on a different single integration test, none related to its PR. That is not three coincidences — it is a population, and it is costing roughly a CI cycle on every second PR.

The shape

The failures concentrate in two areas:

  • Live-model tests. DelegationSubThreadUsageTest.DelegatedSubThread_RecordsUsageUnderRealModel_AndPricesNonZero (a TimeoutException against claude-haiku-4-5) and OrleansSubThreadAutoResumeTest (asserting a live model produced ≥3 streaming snapshots; it saw 2). Both assert on the behaviour of an external service: its latency, and how it chooses to chunk a stream. Neither is a property of this repo, and no change here can make them deterministic.
  • Orleans activation paths. Assertions of the "expected to have happened by now" shape.

Observed evidence of the population: DelegationSubThreadUsageTest failed one PR's first run and the same diff went fully green on the next run; six distinct tests failed across ten runs on unrelated branches.

Why this needs its own issue rather than per-test triage

These are not on the known-unrelated list, so each one costs a fresh investigation to attribute — and the attribution is always the same answer. Worse, the cost is not just time: a red run that is not the PR's fault trains everyone to re-run reds, which is exactly the habit that hides real races. Two genuine defects were found this week because someone declined to re-run (a store-key defect where the test was green only when production was broken, and a NACK that slandered healthy hubs). That discipline stops paying if half of all reds are noise.

Known populations this is NOT

Distinguish before attributing anything here:

The six here are none of those.

What would settle it

  1. Decide whether live-model assertions belong in the gating suite at all. Testing real pricing and real streaming has genuine value, but an assertion whose outcome depends on an external API's latency and chunking cannot gate a merge. A separately-scheduled run that is allowed to fail without blocking a PR would keep the coverage and remove the tax. Note ~3 streaming snapshots is an assumption about model behaviour, not a contract — the model is free to chunk differently on any given call.
  2. Quantify the rest. A sweep of failed shards over the last week, bucketed by test, would say whether the Orleans-activation half is one race or several — the same technique that turned CI: one Roslyn Emit poisons the whole process — every later compile NREs in Cci.MetadataWriter.GetConsolidatedTypeParameters, shard ends exit=124 #890 from "a flaky project" into 14 attributable occurrences with a rising rate.
  3. Do not fix any of these by raising a timeout or a retry count. For the live-model ones the bound is not the problem; for the activation ones a widened bound hides a real race.

Provenance

Surfaced while landing #1373/#1374 (export defects). Both PRs hit it — one on its first run, greening on the next with an unchanged diff — which is what prompted the scan across branches rather than treating it as a local nuisance.

Activity

  1. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    The sweep (item 2), and a falsified premise (item 1)

    Bucketed every failed MeshWeaver Build and Test run since 2026-08-08 — 300 runs — by failing
    test name, from each shard's Test failed annotations, deduped per run.

    window failed runs with ≥1 test failure failed on exactly ONE test
    08-08 19:15 → 08-13 05:05 (4.4 d) 300 242 168 (56 %)
    08-12 00:00 → 08-13 05:05 (29 h) 94 72 51 (54 %)

    The shape is real and steady — just over half of all reds are a single test — and bigger than
    "six of ten". But the 51 recent singletons spread across 30 distinct tests, so the answer to
    "is the Orleans-activation half one race or several" is: several. Nothing in the tail shares a
    root with anything else in it.

    Last 29 h, occurrences | distinct branches:

    # br test note
    8 3 FutuReAnalysisTest.EuropeRe_AnnualReport_EmbeddedCharts live. No renderer is registered for area KeyMetrics, preceded by '$type':'AnalysisContent' is NOT registered in this hub — a dynamic-NodeType registration race. NOT #613 (that is exit=139 SIGSEGV, a different signature in the same project)
    6 4 SkillAutocompleteTest.Autocomplete_ListsBuiltInSkills_FromCatalog appears fixed — last seen 08-12T09:36
    4 4 FrameworkStaleProactiveRebuildTest.…ProactivelyRebuilds… live (→08-13T03:09), on four unrelated branches incl. main
    3 3 ThreadAgentIntegrationTest.FullFlow_… live (→08-13T01:24)
    2 2 OrphanedStartingExecutionRecoveryTest, NodeTypeModelProbeTest, LogIncidentIngestAccessContextTest, CrossHubPatchAtomicityTest live
    1 1 22 further tests tail

    Two big buckets over the full 4.4 days are already dead and should not be re-chased:
    OrleansBrokenNodeTypeAccessTest (16, all 08-09/08-10) and StaleStampRootBindingTest (6, all
    08-10). #890 / #613 / #1379 excluded throughout, not double-counted.

    Item 1: the two named tests are not live-model tests

    Neither calls a model. Both build a fake in-process IChatClient.

    • DelegationSubThreadUsageTest — claude-haiku-4-5 is a string used to look up a
      ModelPricing rate; the seeded provider endpoint is literally https://example.invalid/v1.
    • OrleansSubThreadAutoResumeTest — the sub-agent emits a fixed 320-char report in 5-char chunks
      20 ms apart (≈1.3 s through ThreadExecution's Sample(100 ms) ⇒ ≈13 writes). ≥3 snapshots
      is an assertion about this repo's streaming path, not about model chunking.

    So there is nothing to move out of the gating path, and the coverage keeps its bar. Both were
    ordinary in-repo races; #1385 fixes them with deterministic waits, no bound raised, no assertion
    loosened:

    • Delegation usage: the test polled GetMeshNodeStream({subThread}/_Usage/{model}) — a POINT
      read of a not-yet-existing node. That NotFound opens MeshNodeStreamCache's storm-breaker
      window on the exact path RecordUsage was about to accumulate into, and the breaker fast-fails
      writes too. Runs 31666562025 / 31645997822 log the chain in order: RouteMessage: NotFound … /_Usage/claude_haiku_4_5 → [TokenUsage] RecordUsage failed … No node found → 45 s
      polling a satellite that exists with zero tokens. The test's own read broke the write it was
      waiting for. Fixed by reading through the live children query (empty-on-absent), the primitive
      ThreadTokenUsageTest.WaitForUsage already documents after AI.Test is not parallel-safe: cross-class shared state (repro in 100s via DOTNET_PROCESSOR_COUNT=4) #1040.
    • Orleans auto-resume: the progress window closed on IsExecuting: false && Messages.Count >= 2. IsExecuting is Status is StartingExecution or Executing, so it is false for the INITIAL
      Idle state — the same snapshot that satisfied the earlier Messages.Count wait, replayed by
      the shared handle. Run 31667783996's two snapshots were "Allocating agent..." and
      "Generating response..." — framework placeholders, zero agent output. Fixed by closing the
      window on the response cell reaching ThreadMessageStatus.Completed, which cannot be true
      before the round starts (16 snapshots locally now, was 2).

    Follow-up worth its own issue

    The storm breaker fast-failing writes is by design, and it relies on the change-feed
    ResetFailureState to heal a path after a real CreateNode. In these runs the window was still
    open when RecordUsage's post-create Update ran — either RecordNegative latched a probe
    failure that landed after the create's invalidation, or the invalidation never reached it. I could
    not discriminate from the CI logs and did not guess at a fix. Repro recipe: point-read
    {thread}/_Usage/{model} while absent, run a usage-emitting round, assert the satellite
    accumulated. Production is not currently exposed — ThreadTokenChip reads _Usage only through
    the children query; the test was the sole point-reader.

  2. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Correction to my own sweep: it systematically UNDER-counts, and #1385's first CI run showed how

    The bucketed numbers above count only failures recorded in a trx. They therefore miss every
    failure inside a project whose host was killed — and that is a large, separate slice:

    runs
    failed runs, 08-08 → 08-13 300
    …with ≥1 trx test failure (bucketable) 242
    …with NO trx test failure at all 58

    Those 58 are the crash/timeout/build/plugin-gate population. When a project hits the 8 min
    wall-clock cap, xUnit is killed mid-run: completed tests stream green, in-flight and not-yet-run
    tests silently vanish from the count
    , and the shard fails only via the exit-marker gate — which
    emits no Test failed annotation and therefore no test name. So the singleton figures are a
    floor, not a census.

    #1385's own first CI run is a clean demonstration. It went red on shard 1 with:

    [CI] MeshWeaver.Hosting.Monolith.Test (part 1/2) exit=124 TIMEOUT (8m wall-clock cap hit)
    

    No Test failed annotation anywhere, and the per-shard Test Results (shard 1) reporter said
    pass. Only the raw log names the culprit — ReleaseRequestWatcherHighWaterTest. BurstOfTwoReleaseTriggers_SecondTriggerIsNotLost, which burned 50 s failing on a corrupt dynamic
    assembly:

    ReflectionTypeLoadException: Unable to load one or more of the requested types.
    Could not load type 'HighWaterLayoutAreas' from assembly
      'DynamicNode_TestData_HighWaterType' because the format is invalid.
    Could not load type 'MeshWeaver.Graph.Generated.TestData_HighWaterTypeMeshNodeProviderAttribute'
      … because the format is invalid.
    NodeType 'TestData/HighWaterType' PARKED after compile failure
    

    "because the format is invalid" on a just-emitted assembly is a torn/partial read of the compiled
    file, not a compile error.
    It is worth checking whether the assembly emit path has the same
    non-atomic-write hazard that AtomicFileWrite (added to FileSystemStorageAdapter /
    FileSystemVersionStore on main today) was introduced to fix for JSON — the assembly write does
    not appear to go through it. This is plausibly the same root as the other live NodeType-compile
    entries in the tail (FrameworkStaleProactiveRebuildTest, and FutuReAnalysisTest's
    '$type':'AnalysisContent' is NOT registered → No renderer is registered for area KeyMetrics),
    but I have not proven that and am not asserting it.

    Two process notes this run also confirms:

    • The branch was 24 commits behind main when it went red. Per AGENTS.md that is the first thing to
      fix, not investigate — merged main and re-ran; the failing project is untouched by Two CI flakes that were neither flaky nor live-model — plus the bucketed sweep (#1384) #1385's diff
      (shards 0 and 5, which carry the two tests it changes, both passed).
    • ReleaseRequestWatcherHighWaterTest never appears in the bucket tables above because its
      project timed out. Any future sweep of this population needs to parse the exit markers and the
      raw shard logs, not just the annotations.
  3. added 2 commits that reference this issue on Aug 13, 2026
  4. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Null result on the two largest live buckets — plus a mechanism for the FutuRe one, and two corrections to the tail

    I went after the buckets in the order the tail ranks them. I did not reproduce any of them on this
    macOS host
    , so no fix is proposed from a guess. What I did get is a concrete mechanism for the
    FutuRe bucket that also explains why it is CI-only, and two corrections to the tail itself.

    What I tried, and what it cost

    bucket attempts result
    FutuReAnalysisTest.EuropeRe_AnnualReport_EmbeddedCharts (8|3) 3 configurations — cold NodeType cache ×3 (the shared /tmp/MeshWeaverFutuReTests/.mesh-cache re-pointed per run, verified genuinely cold: the run re-emitted FutuRe_LocalAnalysis_*, FutuRe_GroupAnalysis_*, FutuRe_BusinessUnit_*, FutuRe_LineOfBusiness_*), at DOTNET_PROCESSOR_COUNT=4, at =2, and once through CI's native xUnit v3 host (dotnet MeshWeaver.FutuRe.Test.dll) rather than dotnet test 59/59 green every time, 21 s / 28 s / 29 s
    CodePackageInstallTest (fresh, see below) whole project, DOTNET_PROCESSOR_COUNT=4, Release 270/270 green, 87 s

    The local wall-clock is the tell: the whole FutuRe suite finishes in 21–29 s here, while the CI
    failure is a 50 s wait that never satisfies. The window is not CPU count — it is the cold Roslyn
    compile being slow enough on a 2-core runner to cross a 5 s threshold that never gets crossed
    here. See below.

    Mechanism for the FutuRe bucket (not proven — but it predicts every observed symptom)

    NodeTypeEnrichmentHelpers.CreateCompilationInProgressConfiguration — the compilation-in-progress
    overlay added by 74eddc6 (2026-08-11, "live compile progress instead of a silent park") —
    registers the progress page for Overview and nothing else:

    => config => config.AddLayout(layout =>
        layout.WithView(MeshNodeLayoutAreas.OverviewArea, (host, _) =>
            NodeTypeLayoutAreas.CompileProgressView(...)));

    So when an instance hub activates while its NodeType is still Pending/Compiling — after the 5 s
    InFlightOverlayGrace — it settles onto a hub on which every area except Overview has no
    renderer, and LayoutDefinition.BuildNotFoundControl serves:

    **Area not found**
    
    No renderer is registered for area `KeyMetrics` on hub `FutuRe/EuropeRe/Analysis`.
    

    That is verbatim the reported failure string. The companion warning is the same cause seen from
    the other side: the overlay composes the mesh DEFAULT node-hub config, so
    LocalAnalysisConfig's .WithContentType<AnalysisContent>() never runs on that hub — hence
    '$type':'AnalysisContent' is NOT registered in this hub. One cause, both symptoms.

    Why CI-only: the overlay only engages if the compile is still in flight at 5 s. FutuRe/LocalAnalysis
    compiles in well under a second on this host even from cold, so the grace never expires and the
    overlay never appears — which is exactly why three cold-cache configurations here are green.

    Why it does not simply heal: ArmOverlaySelfHeal's fast path needs the type node's version to
    advance
    past the one captured at overlay time; its fallback is SelfHealGrace = 45 s, and
    GetControl's first wait is Math.Max(timeoutSeconds, ActivationBudgetSeconds) = 50 s. A heal
    on the grace path has to land a recycle, a re-activation and a re-render inside the remaining ~5 s.
    That is the razor's edge the 8 failures sit on, and it is consistent with them being intermittent
    on the same three branches rather than deterministic.

    The fix I would propose (deliberately not shipped from here — unverifiable without the repro,
    and it touches a live feature): give the overlay a catch-all so every area of an overlaid instance
    renders the progress page instead of a false "Area not found". The sanctioned shape already exists
    in the tree — KernelContainer.cs:87, layout.WithView(ctx => !layout.HasNamedRenderer(ctx.Area), …),
    with the HasNamedRenderer guard that stops a catch-all wiping named areas. This is worth doing
    independent of the flake: today a deep link to any non-Overview area during a framework-bump
    sweep tells the user the area does not exist, which is precisely the silent-park failure 74eddc6
    set out to remove — just relocated from Overview to everywhere else.

    ⚠️ Whoever takes it: the change turns the terminal-looking **Area not found** frame into a
    transient progress frame (and, on Ok, a RedirectControl). FutuReAnalysisTest.IsAreaNotFound —
    the "keep waiting, this is transient" filter — must learn both, or the bucket converts from a
    timeout into a latched-wrong-frame failure on Should().Contain("Total Premium").

    Correction 1 — StaleStampRootBindingTest is NOT dead

    The tail lists it as 6 occurrences "all 08-10, do not re-chase". It failed again today, on
    main, run 31683003325 (commit 891aae06a, 08-13T08:39Z), shard 4 — and with a different
    signature, so it is a second root wearing the same test name:

    [08:53:04.001] [Warning] [PackageInstaller] root ShopA never answered after its recycle —
                   continuing; whatever touches it next may still land on a hub that is tearing down
    install returned at +20575ms          ← PackageInstaller.RootReadyTimeout burned in full
    System.TimeoutException  at StaleStampRootBindingTest.cs(327)   ← then the Tests-area wait
    [08:53:04.043] JsonSynchronizationStream: resubscribe failed.
                   DeliveryFailureException: Hub ShopA is shutting down (RunLevel=Dead)
    

    WaitForRootReady pings the recycled root until it answers and gives up gracefully after
    RootReadyTimeout. Here the root did not re-activate within 20 s and was still RunLevel=Dead
    afterwards — i.e. a hub-teardown wedge, not a stale-stamp binding problem. Note the graceful
    .Catch is what converts that wedge into a later, differently-named test failure.

    Correction 2 — the "format is invalid" cluster survived #1387

    #1387 fixed the non-atomic publish in both IAssemblyStore implementations. The same signature
    recurred on #1387's own merge commit — run 31683641381, commit e45466ee6, 08-13T08:47Z:

    CodePackageInstallTest.CodePackage_InstallsNodeTypeAndSource_CompilesLive
      Failed to load the compiled assembly — Could not load type 'Widget' from assembly
      'DynamicNode_type_widget-type' because the format is invalid.
      … Failed to extract NodeTypeConfigurations from
      /tmp/meshweaver-test-mesh-cache-4693-CodePackageInstallTest/
        type_widget-type_8def91993854d3e_48a950f45c2c4dad89d6b122a275c6f1/type_widget-type.dll
      NodeType 'type/widget-type' PARKED after compile failure
    

    That path is the compilation cache, not the assembly store — {cacheDir}/{nodeName}_{ticks}_{guid}/,
    i.e. a directory published by EmitToDiskWithRetry, whose publish is already atomic
    (.staging-… dir → Directory.Move) and whose lambda wiring I checked is correct
    (CompileToDiskAsync really does pass the staging dir through). So this occurrence is not
    explained by #1387's defect and not by a torn write at that site. There is a second, still-open
    root behind "format is invalid". Its post-publish verification is only File.Exists && Length > 0,
    which is weaker than "the image is complete" — worth a look, but I could not prove it.

    One latent defect found while looking (no flake attached)

    MeshNodeCompilationService.CompileToReleaseAsync + LoadAndExtractConfigurationsFromReleaseAsync
    have no callers anywhere in src/ or test/ — but CompileToReleaseAsync does
    Directory.CreateDirectory(releaseFolder) then File.Create(dllPath) + compilation.Emit(...)
    straight onto the final name, and releaseFolder is GetReleaseFolderPath =
    {cacheDir}/{nodeName}_{releaseKey} — which matches the live {nodeName}_* glob
    TryGetLatestCachedDllPath enumerates, ordered by directory write-time (so a folder being written
    right now sorts newest), while IsReleaseValid is satisfied by a 0-byte DLL. That is the exact
    hazard EmitToDiskWithRetry's own comment describes and guards against, unguarded, publishing
    executable bytes. It causes nothing today because it is dead — but it sits next to the corrected
    path as a working-looking template. Delete it or route it through the same staging+move.

  5. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Follow-up: one root found and fixed — #1408

    My comment above ends on a null result. It was written 20 minutes too early: the bucket I could not
    reproduce locally then reproduced itself on my own PR, and handed over the stack trace.

    #1402 is a workflow-only change — it cannot touch a test outcome — and it went red on
    StaleStampRootBindingTest.StaleStampSelfTypedRoot_RootServesItsTypesArea (run 31686258015,
    09:22Z), 40 minutes after the same test failed on main (run 31683003325, 08:39Z). That is the
    bucket "Correction 1" above says is not dead. This time the log named the cause:

    ObjectDisposedException: Cannot load assembly from disposed context
    Object name: 'DynamicNode_ShopA_Front'
       at NodeAssemblyLoadContext.LoadNodeAssembly()
       at MeshNodeCompilationService.CompileResultFromAssembly(...)
    NodeType 'ShopA/Front' PARKED after compile failure — further activations serve the
    cached error without recompiling
    

    Pin() and LoadNodeAssembly() guarded on the same _disposed flag, and Dispose() raised it
    on entry — before draining pins. So a scan could pin the context (proving it was live), have
    Dispose() flip the flag one instruction later and then block waiting for that very scan, and
    get an ObjectDisposedException thrown at it against a LoaderAllocator that was provably still
    alive. CompileResultFromAssembly catches it, writes CompilationStatus.Error, and the watcher
    parks the type with no retry — so a millisecond-wide race causes permanent damage. The #1387
    reasoning exactly, from the opposite side: there the bytes were incomplete, here they were perfect
    and we refused to read them.

    Fixed in #1408 by splitting the flag into _unloading (Dispose started — refuse new scans) and
    _disposed (drain over — refuse loads), with the relaxation applying only to a pinned caller so
    an unpinned one keeps today's strict behaviour. Deterministic fail-before/pass-after test; the
    before-run reproduces the CI message verbatim. MeshWeaver.Graph.Test 1098/1098,
    MeshWeaver.PluginCatalog.Test 270/270, MeshWeaver.FutuRe.Test 59/59.

    Still open from this sweep, unchanged by #1408 and worth separate issues:

    1. The second root under the same test name — the 08:39Z failure is a hub-teardown wedge, not
      this one: [PackageInstaller] root ShopA never answered after its recycle, RootReadyTimeout
      burned in full (20 s), the root still RunLevel=Dead afterwards. Different failure, same test.
    2. The FutuRe bucket (8|3) — mechanism and proposed fix in my comment above (the
      compilation-in-progress overlay registers a view for Overview only, so every other area of an
      overlaid instance serves a false "Area not found"). Not reproducible on this host; three
      cold-cache configurations green.
    3. "format is invalid" after A NodeType assembly can be loaded mid-write — 'format is invalid' parks the type, and the emit path skips AtomicFileWrite #1387 — recurred from the compilation-cache path, whose publish is
      already atomic. Second root still unexplained.
  6. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Index: where each bucket from the sweep now lives

    Short pointer comment, because the table in the sweep above is what a future triager will read and two of its rows are now wrong.

    bucket in the table status now home
    SkillAutocomplete 6|4 fixed, as recorded —
    FrameworkStaleProactiveRebuild 4|4 fixed — #1368 closed 08-13T03:14Z, after the sweep window ended #1368
    🔴 StaleStampRootBindingTest — "6, all 08-10 … do not re-chase" NOT dead. Fired twice on 08-13 (main 08:39Z, and a PR 09:22Z) and carries two distinct roots root A (ObjectDisposedException, PARKS the type) → fixed in #1408 · root B (recycle/teardown wedge) → #1405
    FutuReAnalysisTest.EuropeRe_AnnualReport_EmbeddedCharts 8|3 mechanism identified, not reproducible on a dev host (3 configurations) #1411
    OrleansBrokenNodeTypeAccessTest 16 dead, as recorded —

    Plus two things the sweep could not have known:

    On the sweep being a floor

    Your correction about the 58 trx-less runs holds, and I hit the same wall from the other side: the two highest-ranked live buckets were not reproducible on an M-series Mac — FutuRe went 59/59 green in three configurations (verified-cold NodeType cache at DOTNET_PROCESSOR_COUNT=4, at =2, and through CI's native xUnit v3 host), and CodePackageInstallTest went 270/270. Both need the 2-vCPU runner's interleaving.

    flake-repro.yml is the instrument for that and it is under-used — it loops one test on the real runner. I am using it right now as a controlled A/B (same filter, same iteration count, main vs a branch) to attribute a failure rather than argue about it. That pattern generalises to the rest of this tail: for any bucket that will not reproduce locally, dispatch it on both arms instead of re-running the PR.

  7. added a commit that references this issue on Aug 13, 2026
  8. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    A sweep that parses exit markers — and the largest live bucket REPRODUCED on a dev host

    Two things this adds to the thread: the sweep redone the way your own correction says it has to be
    done (raw shard logs + exit markers, not annotations), and — for the first time in this issue — a
    local reproduction with a measured rate of the bucket that sweep ranks first.

    1. The sweep, with exit markers

    33 failed MeshWeaver Build and Test runs since 2026-08-13T00:00Z. For every failed
    Run tests (shard N) job I pulled the raw log and parsed BOTH [FAIL] test names and the
    [CI] <project> exit=<n> <VERDICT> markers.

    runs
    failed runs in window 33
    …with ≥1 [FAIL] (annotation-visible) 26
    …with ≥1 non-zero exit marker 27

    Exit markers: 11 × Hosting.Monolith.Test TESTFAIL, 6 × PluginCatalog.Test, 4 × Hosting.Orleans.Test,
    3 × AI.Test, 2 × Graph.Test, 1 × Threading.Test, 1 × InstanceSync.Test, 1 × exit=124 TIMEOUT
    (#890), 1 × exit=139 SIGNAL (#613) — plus 3 of a verdict the annotation sweeps never had a name for:

    [CI] MeshWeaver.Hosting.Monolith.Test (part 2/2) exit=1 MASKED
         (trx records 3 failing test(s), which does not explain exit=1 — host crashed after streaming results)
    

    That is the trx-less population being caught in the act rather than inferred: the shard does have a
    trx, its failures are attributable, and the host still died afterwards. Worth its own look —
    the named failures in those runs are the ones below, so whatever kills the host is not what fails the
    tests.

    2. Rank by BRANCH SPREAD, not by count

    Raw counts mislead here, because a PR whose own diff breaks a test contributes several occurrences on
    one branch. Filtering the window's buckets that way:

    bucket occ | branches verdict
    DynamicTypePreWarmerTest ×3 tests 6 | 1 all on fix/1391-nosources-default-queries, twice — that PR's own regression, not a flake
    StoreLevelWriteConflictTest 2 | 1 fix/1284-activity-node-shape; asserts "ActivityLog", got "Activity" — that PR's own rename
    LayoutAreaRetrievalTest 2 | 1 same branch twice
    FrameworkStaleProactiveRebuildTest.…ProactivelyRebuilds… 3 | 3 all 00:17–03:09Z; #1368 merged 03:14Z → consistent with fixed, none after
    NodeTypeCompileParkTest ×3, ReleaseRequestWatcherHighWater, LinkedInProfileLayoutArea, FrameworkStaleRecord… 6 | 1 ONE run on fix/1384-flake-sound-waits — a single shard blow-up, not six buckets
    StaleActivationDurableFirstSeedTest.ReactivationViaRead_ServesDurableTruth_AndWritesNothingBack 3 | 3 live, unrelated branches, spans the window
    OrleansGrainTeardownStragglerTest 2 | 2 live (teardown family — likely #1405/#613 adjacent)
    PluginDependencyOrderTest 2 | 2 live

    So the largest genuinely live, cross-branch bucket in the window is
    StaleActivationDurableFirstSeedTest.ReactivationViaRead_ServesDurableTruth_AndWritesNothingBack —
    fix/1311-… 00:28Z, fix/1362-silent-read-nack 00:38Z, fix/1284-activity-node-shape 11:28Z.
    Not #890 (named test, exit=1, no NRE), not #613, not #1405's recycle wedge (it completes in ~2 s).

    3. It reproduces — 1 in 60

    All three CI occurrences carry the identical assertion, to the word:

    Expected value to be 2 because activation is a READ — persisting the just-loaded state back is the
    Systemorph/MeshWeaver#590 activation write-back (v979: stale content re-persisted by system-security during activation),
    but found 3.
    

    i.e. a pure read-reactivation wrote one extra row to storage, which is exactly the #590 invariant
    the test exists to pin. On an M-series Mac, native xUnit v3 host, DOTNET_PROCESSOR_COUNT=2:

    cd test/MeshWeaver.Hosting.Monolith.Test/bin/Debug/net10.0
    DOTNET_PROCESSOR_COUNT=2 ./MeshWeaver.Hosting.Monolith.Test \
      -class MeshWeaver.Hosting.Monolith.Test.StaleActivationDurableFirstSeedTest
    

    in a loop → 1 failure in 60 iterations (≈1.7 %), same assertion, same 2 vs 3. ~3.5 s per
    iteration, so a full measurement is minutes, not a CI cycle. This is the first bucket in this issue
    that does not need the 2-vCPU runner.

    4. …and it is a Heisenbug with respect to logging

    Raising MeshWeaver.Graph.MeshNodeTypeSource / MeshWeaver.Graph.SaveMeshNodeHandler /
    MonotonicWriteGuardStorageAdapter to Debug in the test's bin/…/appsettings.json (the
    sanctioned way — no src log-level edits) and re-running the same loop went 160 iterations clean,
    under more machine load than the run that reproduced it. At p = 1/60 that is P ≈ 7 %. So the
    window this race lives in is narrow enough that the debug writes close it — which is worth knowing
    before anyone spends a day log-diving it.

    5. What the failure needs in order to be attributable

    Gated.WriteCount(path) counts writes that reached the innermost adapter; the assertion reports only
    the count. "found 3" says a write happened and nothing about which, and the two candidate
    mechanisms are distinguishable purely by the version it carried:

    • an activation write-back — the just-seeded durable row (v7000) echoed back, i.e. the
      OwnNodeCache.PersistedSnapshot reference-identity suppression in
      MeshDataSource.InstallPersistenceSampler failing open. Note that stamp is a single
      last-write-wins slot
      written by MeshNodeTypeSource.BuildInstanceCollection on every accepted
      own-node emission, while the sampler consumes emissions asynchronously — so two accepted emissions
      in the activation window are enough for the first to stop matching the slot.
    • a late dispose-flush from the recycle (v1) landing after writesBefore was sampled — which
      would have to have passed the monotonic guard, so it is the less likely of the two.

    I am carrying a one-line change to GatedReadStorageAdapter that records v{version}/{name} per
    write and puts the trace in that assertion message, so the next occurrence — local or CI — names the
    offender instead of leaving it at "found 3". Happy to send it separately if it is wanted before the
    root cause.

    Provenance

    Found while landing #1412 (the "format is invalid" second root — PR #1423). Sweep script, raw logs
    and the loop harness are reproducible from the recipe above.

  9. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Follow-up on the StaleActivationDurableFirstSeedTest bucket: the extra write is a version MINT, and all three CI failures agree

    Correcting and sharpening my comment above before anyone spends time on it.

    The one number that identifies the write

    The test advances durable truth out of band to Version = 7000 and then does nothing but a read.
    All three CI failures log the same line during the reactivation:

    MeshNodeTypeSource[TestData/read-reactivation-…]: DROPPED a stale own-node emission (Version=1)
    below the version this hub already holds (Version=7001).
    

    7001, not 7000 — in 31654549526, 31655126436 and 31694736256 alike. Nothing in the test ever
    writes 7001. So the extra write the assertion catches is not an echo of the durable row: it is a
    freshly minted revision, NextVersion(7000), produced inside the activation window and then
    persisted by the own-node persistence sampler. That also explains the absence of any
    [MonotonicWriteGuard] CONFLICT line in those runs — 7001 > 7000 sails through the guard, so the
    write reaches the innermost adapter and is counted.

    Two things follow, and both narrow the hunt a lot:

    • it is not the recycle's dispose-flush landing late — that would carry v1, lose to the guard,
      and never reach the adapter (WriteCount counts only writes that arrive);
    • it is not a plain PersistedSnapshot reference-identity miss on the seed either — that would
      re-persist 7000, not mint 7001.

    The mint has to come from MeshNodeTypeSource.UpdateImpl's update re-stamp:

    var alreadyAdvanced = u.Node.Version > u.PreviousVersion && (!isOwn || u.Node.Version >= ownFloor);
    return u.Node with { Version = alreadyAdvanced ? u.Node.Version : MeshNode.NextVersion(floor) };

    NextVersion(7000) requires alreadyAdvanced == false, i.e. a genuine CONTENT change arriving at
    a version that is not above the one already held
    — the diff a few lines up compares content with
    !ex.Equals(nv with { Version = ex.Version }), so a version-only difference is already excluded. In
    other words: something committed a node whose CONTENT differs from the seeded one while carrying the
    SAME version 7000.

    The candidate that fits the shape is the enrichment graft. Initialize seeds the workspace with
    GraftRoutingEnrichment(durableRow) — durableRow with { HubConfiguration = routing.HubConfiguration }
    — while every other route to the own node (notably the storage change-feed reconcile in
    MeshDataSourceExtensions.SubscribeToOwnDeletion, which commits notification.Entity verbatim)
    carries the RAW row. Grafted and raw differ by that delegate and by nothing else, and record
    Equals counts the delegate, so raw-vs-grafted at the same version reads as a content change →
    re-stamp → 7001 → the sampler persists it. I have NOT proven that leg fires here, which is why I am
    posting it as the lead rather than a fix.

    Reproduction, honestly

    • On the unmodified tree: 1 failure in 60 iterations (DOTNET_PROCESSOR_COUNT=2, native xUnit v3
      host, -class StaleActivationDurableFirstSeedTest), the exact CI assertion.
    • Then it stopped reproducing: 160 iterations clean with MeshNodeTypeSource /
      SaveMeshNodeHandler at Debug, and a further 259 clean with logging back at baseline but a
      write-recording line added to GatedReadStorageAdapter. At p = 1/60 those are P ≈ 7 % and ≈ 1 %.

    So: the bucket does reproduce on a dev host, which is new for this issue — but the window is
    narrow enough that a log write or an extra ConcurrentQueue.Enqueue on the write path closes it.
    Anyone measuring a fix here needs to measure the BEFORE on the same tree they measure the AFTER on,
    and should expect a few hundred iterations per arm.

    The cheap thing that would settle it

    Gated.WriteCount(path) reports a count; the assertion says "found 3" and stops. Recording
    v{version}/{name} per write in GatedReadStorageAdapter and putting the trace into that assertion
    message turns the next occurrence — CI or local — into a direct answer: v7000/durable-advance, v7001/durable-advance confirms the mint, v1/created would refute it and point back at the
    dispose-flush. It is ~15 lines in a test-only helper. Say the word and I will send it on its own.

  10. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    The grafted-vs-raw leg does NOT fire — it cannot. And the mint has a much better candidate, one line away.

    Taking up the lead from the comment above ("I have NOT proven that leg fires here, which is why I am posting it as the lead rather than a fix"). It is refuted, and refuting it points straight at something else.

    1. Refuted — MeshNode.Equals deliberately excludes HubConfiguration

    The lead rested on this step:

    Grafted and raw differ by that delegate and by nothing else, and record Equals counts the delegate, so raw-vs-grafted at the same version reads as a content change → re-stamp → 7001

    MeshNode does not use compiler-synthesized record equality. It hand-writes Equals, and the delegate is explicitly left out — src/MeshWeaver.Mesh.Contract/MeshNode.cs:387-412, with the exclusion stated in its own doc comment:

    The runtime-only wiring (HubConfiguration, GlobalServiceConfigurations) is [JsonIgnore]/[NotMapped], reference-typed (a Func / a list of Func), and not part of the node's persisted value — it is deliberately EXCLUDED from value equality.

    GraftRoutingEnrichment changes exactly one field (durableRow with { HubConfiguration = routing.HubConfiguration }), and that field is the one Equals ignores. So UpdateImpl's update filter

    !ex.Equals(nv with { Version = ex.Version })

    is false for a grafted/raw pair at the same version: not an update, no re-stamp, no mint. Verified rather than reasoned — a new fact asserts it in exactly that shape (MeshNodeContentEqualityTest.RoutingGraft_VersusRawRow_AtSameVersion_IsNotAContentChange, PR below), so a later switch to synthesized equality cannot silently reintroduce the mint.

    Worth keeping: the premise of the lead was right. Grafted and raw genuinely do meet — InMemoryStorageAdapter.Write strips HubConfiguration before storing and before publishing (:98-99), so every change notification carries a raw row while the activation seed carries a grafted one. It just isn't a content change.

    Also refuted, same evidence: the stale v1 routing snapshot cannot reach UpdateImpl either. The routing-supplied own-node stream has exactly two consumers — the ctor enrichment capture (MeshNodeTypeSource.cs:156-162, which only writes _latestRoutingNode and never touches the workspace) and Initialize's Concat leg, which is gated by AcceptOwnNodeEmission. There is no third subscriber. So UpdateImpl is not where 7001 comes from at all.

    2. What does mint 7001 — the change-feed reconcile, via UpdateOwn

    MeshDataSource.cs:1394-1398, the storage.Changes reconcile in SubscribeToOwnDeletion:

    hub.GetWorkspace().GetMeshNodeStream()
        .Update(current =>
            current is not null && current.Version >= newNode.Version
                ? current
                : newNode)

    GetMeshNodeStream() with no path is the OWN node ⇒ UpdateOwn, which mints unconditionally (MeshNodeStreamExtensions.cs:839-842):

    updated = updated with { Version = MeshNode.NextVersion(Math.Max(current.Version, updated.Version)) };

    With current = the pre-recycle node (v1) and updated = the notification's newNode @7000 ⇒ NextVersion(7000) = 7001, carrying Name = "durable-advance". That is the number, from a single expression.

    Every gate on the way is passed by design:

    • echo-suppression newNode.Version == lastSelfWrite.Value — lastSelfWrite is the pre-recycle version (1), ≠ 7000;
    • the forward-only guard current.Version >= newNode.Version — 7000 > 1, so it adopts, which is its whole purpose;
    • UpdateOwn's no-op gates (:817-818) — Name and Version differ;
    • OwnNodeCache.PersistedSnapshot reference identity — the adopted node is a fresh with-copy;
    • MonotonicWriteGuard — 7001 > 7000 sails through, which is why no CONFLICT line appears in any of the three CI runs.

    Then .Do(pending.Track) sits before the Sample (MeshDataSource.cs:1168), so PendingOwnSave holds node@7001 immediately and either the 200 ms sample tick or FlushPendingOwnSave at dispose writes it. One write, which is exactly the "expected 2, found 3" the assertion reports.

    Which hub? The old one. MessageHub.Dispose only posts ShutdownRequest(Quiescing) and returns; delSub — a subscription that can write — is not torn down until DisposeImpl at the ShutDown phase. RecycleOwner never awaits disposal, so the test's out-of-band Storage.Write(…Version = 7000) lands while the recycled hub's reconcile is still listening. IsolatedChangeFeed does not replay, so the new hub can never see that notification — which is why this is the old hub's doing and why it is a race at all.

    3. This is now reproduced locally, and the timing confirms the account

    1 failure in 250 iterations on the unmodified tree at today's main (4e8e2efd7, ~130 commits past the last measurement), DOTNET_PROCESSOR_COUNT=2, native xUnit v3 host, loglevel=Warning throughout — the CI assertion, verbatim, expected 2 … found 3. Same recipe as the earlier 1-in-60; the two rates are not distinguishable at these sample sizes, so read it as "still roughly 1 in 100–200", not as a change.

    The reproduction settles the sequencing, because of when the drop line fires:

    [TEST START]                     16:03:42.959
    [recycle] disposed owner hub for TestData/read-reactivation-0e736d04…
    [Warning] MeshNodeTypeSource[…]: DROPPED a stale own-node emission (Version=1) below the
              version this hub already holds (Version=7001).          16:03:43.380
    [TEST FAILED: … found 3]         16:03:45.505
    

    421 ms in, immediately after the recycle and the out-of-band write — and ~2 s before the reactivation's assertions. The floor is already 7001 at that point, i.e. the mint happened around the out-of-band write, not during the reactivation. That matches the old-hub reconcile exactly, and it does not match anything on the new hub's activation path (whose seed cannot be above 7000 unless something already wrote 7001).

    4. Why the number was over-read (mea culpa on my own earlier comment)

    The comment above treats 7001 as strongly diagnostic:

    NextVersion(7000) requires alreadyAdvanced == false, i.e. a genuine CONTENT change arriving at a version that is not above the one already held

    That inference assumed the mint site was UpdateImpl. Through UpdateOwn the arithmetic is different and less specific: NextVersion(Math.Max(current.Version, updated.Version)) yields 7001 for any current ≤ 7000 — v1 gives 7001 just as v7000 does. So 7001 never discriminated "a content change at the held version" from "adopting 7000 from below". It only ever said Math.Max(...) == 7000.

    5. The root cause, stated as a shape

    Adopting an already-durable external write is an observation, and an observation must not mint. The reconcile exists to move the in-RAM node forward to match a write someone else already persisted; routing that through UpdateOwn stamps the in-RAM node one revision above the durable row with identical content, and the persistence sampler then writes that phantom revision back. That is an activation/observation-window write-back — the very #590 shape this test pins, arriving through the door next to the one it is watching.

    The correct shape already exists a few lines away, in BuildInstanceCollection: adopt at the durable version and stamp OwnNodeCache.PersistedSnapshot so the sampler suppresses the echo. AdoptDurableTruth's mint is different and should stay — there the point is to get above a durable row that refused your write.

    I am not shipping that change here. It sits in the middle of the cross-hub write-loss family (CrossHubPatchAtomicityTest's lagged-echo defence is the same Update lambda), and at ~1-in-200 a fix needs a few hundred iterations per arm to be worth anything. Whoever takes it should measure BEFORE and AFTER on the same tree, at loglevel=Warning — and know that instrumenting the write path closes the window (a ConcurrentQueue.Enqueue there went 259 iterations clean).

    Two secondary contributors worth separate consideration either way: RecycleOwner does not await disposal, and a subscription that can write survives until the ShutDown phase.

    6. Naming the offender without closing the window — PR #1428

    "found 3" is unattributable, and the obvious fix (log every write) is the one thing that provably destroys the race. It is also unnecessary: the durable row IS the last write that reached storage, and the test already reads it — two lines after the assertion that fails. Moving that read ahead of the assertion puts the answer in the failure message at zero cost to the write path:

    • v7000/'durable-advance' → an unchanged re-persist (a lossless echo)
    • v7001/'durable-advance' → the mint-on-adopt above
    • v7001/'created' → a stale pre-recycle snapshot re-stamped over durable truth

    A read cannot perturb the race, and taking it before the count is sampled only ever widens the window for a late write to be caught.

    PR: test-only, no production change, no assertion loosened, no bound raised.

  11. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Follow-up: the mechanism is now pinned deterministically — no race, no recycle, 100 % of the time in 2.5 s

    My comment above proposed the storage.Changes reconcile as the mint site but rested on a timing argument. It does not need one. Strip the race out entirely and the defect is unconditional.

    The experiment: keep the hub LIVE (no recycle, no gate, no reactivation) and advance durable truth out of band, so the live hub adopts an external write through its own change-feed reconcile. Observation is a read; it must not mint and must not write back.

    [served]        name=external-advance version=5001
    [durable after] name=external-advance version=5001  writes=3 (before=1)
    
    Expected value to be 5000 because adopting an already-durable external write is an
    OBSERVATION — minting a revision above it makes the in-RAM node a phantom the sampler
    then writes back, but found 5001.
    

    Written at 5000, the hub holds 5001, and the durable row is now 5001 too — it wrote the phantom revision back. Reproduces every time, in 2.5 s, at loglevel=Warning. (writes 1 → 3 is create + my own out-of-band write + one hub write-back — the Be(writesBefore) assertion in my probe was sloppy about that baseline; the version assertion and the durable row reading 5001 are the decisive parts and neither depends on the count.)

    So the chain is confirmed end to end, from a single expression:

    // MeshDataSource.cs:1394-1398 — adopt the external write…
    hub.GetWorkspace().GetMeshNodeStream()
        .Update(current => current is not null && current.Version >= newNode.Version ? current : newNode)
    
    // …through MeshNodeStreamExtensions.cs:839-842, which mints UNCONDITIONALLY
    updated = updated with { Version = MeshNode.NextVersion(Math.Max(current.Version, updated.Version)) };

    NextVersion(5000) = 5001; the node is a fresh with-copy so OwnNodeCache.PersistedSnapshot does not suppress it; .Do(pending.Track) sits before the Sample, so the sampler (or the dispose flush) persists it; MonotonicWriteGuard accepts it because 5001 > 5000.

    What this changes about #1384

    The defect is not a race. The race only decides whether it is observed.

    ReactivationViaRead_ServesDurableTruth_AndWritesNothingBack recycles the owner before the out-of-band write, so whether the write-back happens depends on whether the notification still finds a live reconcile subscription — and MessageHub.Dispose() merely posts ShutdownRequest(Quiescing) and returns, tearing disposables (including that write-capable subscription) down only later at the ShutDown phase, which the test never awaits. That is the ~1-in-250 window. In production there is no such window at all: any hub that is simply live when another writer advances its row takes the phantom mint unconditionally.

    Which also means the blast radius is wider than one flaky test: every cross-writer convergence mints a spurious revision and writes it back. Two mirrors, a migration, a repair job, GitSync, or a second silo advancing a node all leave the observer's copy one revision above durable truth and then persist that. It inflates version history with content-identical revisions and it is an activation/observation-window write-back of exactly the #590 shape.

    Repro, ready to paste

    Drop into StaleActivationDurableFirstSeedTest (it reuses that class's Gated / Storage / ReadDurable helpers) and run
    -method …StaleActivationDurableFirstSeedTest.LiveHub_AdoptingAnExternalDurableWrite_MintsNothing_AndWritesNothingBack:

    [Fact(Timeout = 55_000)]
    public async Task LiveHub_AdoptingAnExternalDurableWrite_MintsNothing_AndWritesNothingBack()
    {
        var id = $"live-adopt-{Guid.NewGuid():N}";
        var path = $"{TestPartition}/{id}";
    
        await NodeFactory.CreateNode(new MeshNode(id, TestPartition)
        {
            Name = "created", NodeType = "Markdown", State = MeshNodeState.Active
        }).Should().Within(30.Seconds()).Emit();
        await ReadNode(path).Should().Within(30.Seconds()).Match(n => n is { Name: "created" });
    
        // Hub stays LIVE. Advance durable truth out of band — straight through IStorageAdapter,
        // which publishes the change notification the live hub's reconcile subscription receives.
        const long externalVersion = 5000L;
        var durable = (await ReadDurable(path).Should().Within(30.Seconds()).Emit())!;
        await Storage.Write(durable with
        {
            Name = "external-advance", Version = externalVersion
        }, JsonOptions).Should().Within(30.Seconds()).Emit();
    
        var served = await Mesh.GetWorkspace().GetMeshNodeStream(path)
            .Where(n => n is not null)
            .Should().Within(20.Seconds())
            .Match(n => n.Name == "external-advance", "the live hub must adopt the external write");
    
        await Observable.Timer(TimeSpan.FromSeconds(2)).Should().Within(20.Seconds()).Emit();
        var after = await ReadDurable(path).Should().Within(20.Seconds()).Emit();
    
        served.Version.Should().Be(externalVersion,
            "adopting an already-durable external write is an OBSERVATION — minting a revision "
            + "above it makes the in-RAM node a phantom the sampler then writes back");
        after!.Version.Should().Be(externalVersion,
            "observing someone else's durable write must not write anything back");
    }

    It is not in PR #1428 — a knowingly-red test does not belong on a branch. It should land with the fix, as the fail-before/pass-after.

    The fix, and why I am not shipping it here

    The reconcile's job is to move the in-RAM node forward onto a revision someone else already made durable. That is an adopt-at, not a mint-above. The shape already exists a few lines away in MeshNodeTypeSource.BuildInstanceCollection: take the row at its own version and stamp OwnNodeCache.PersistedSnapshot so the sampler suppresses the echo. AdoptDurableTruth's mint must stay as-is — there the point is to climb above a durable row that refused your write.

    I am leaving it to whoever owns this area, because the Update lambda in question is also CrossHubPatchAtomicityTest's lagged-echo defence (the concurrent cross-mirror data-loss family), and changing adopt semantics there is a scope call rather than a mechanical fix. The good news is that it no longer needs the flake to validate: fail-before/pass-after on the deterministic test above, then the flake arm only as a regression check.

    Measured for whoever picks it up — same tree (4e8e2efd7), DOTNET_PROCESSOR_COUNT=2, native xUnit v3 host, loglevel=Warning throughout:

    arm result
    ReactivationViaRead_… unmodified 1 failure / 250 iterations, CI signature verbatim
    LiveHub_AdoptingAnExternalDurableWrite_… fails 100 %, 2.5 s
  12. rbuergi commented on Aug 13, 2026

    @rbuergi
    ContributorAuthor

    Split the root cause out to #1432 so it does not stay buried in this sweep thread — same convention as #1405 / #1411 / #1412.

    #1432: the storage.Changes reconcile adopts an already-durable external write through UpdateOwn, which mints unconditionally, so the hub lands one revision above durable truth and the persistence sampler writes that phantom revision back. Deterministic, 2.5 s, no recycle and no race — the repro is in the issue.

    This bucket (StaleActivationDurableFirstSeedTest.ReactivationViaRead_…, measured 1 failure / 250 iterations on 4e8e2efd7 at loglevel=Warning) is one observation of #1432, made intermittent only by whether the change notification still finds the recycled hub's write-capable subscription alive — Dispose() posts ShutdownRequest(Quiescing) and returns, and RecycleOwner never awaits the teardown.

    So the row for this bucket: root cause → #1432; PR #1428 carries only the equality pin (refuting the graft lead) and a discriminator that names the offender without touching the write path.

  13. added a commit that references this issue on Aug 13, 2026
  14. 22 remaining items

  15. rbuergi commented on Aug 26, 2026

    @rbuergi
    ContributorAuthor

    Every named member of the population now has a merged root cause or an owner — but the rate this issue exists to measure has not been re-measured, so it stays open

    Following the 13:04Z comment, which enumerated the population as six named tests and put agents on four. All four landed root-cause fixes between 14:30Z and 14:46Z. None widened a bound, added a retry, or weakened an assertion, and each shipped a guard or a failing-first repro:

    member issue merged fix
    LiveQueryHandoffDropTest (the one redding three unrelated PRs at once) #2377, #2319 #2389 — the parameterless IEnumerable.ToObservable() defaults to CurrentThreadScheduler, whose [ThreadStatic] trampoline flag makes emissions queue instead of run when a Task resumes inline inside an Rx pipeline. Initial is then never emitted — no error, no completion, no row. Fixed at all 12 sites on the storage/query read path (including PersistenceService.ListChildPaths, "the walk the PORTAL actually uses"), plus BareToObservableOnReadPathGuard ratcheting it to zero. Measured cold, whole-assembly, --cpus 4: 3 failures / 13 runs → 0 / 40.
    CrossProcessChangeFeedTest #2008 #2392 — a per-node hub wrote back the durable row it had just read as its activation seed; MonotonicWriteGuard then refused a legitimate write and AdoptDurableTruth rebased an owner that never edited anything. New test 10/10 red with the fix reverted, 10/10 green with it.
    MeshWeaver.Threading.Test exit=124 wedge #2341 #2390 — Catch(...).Repeat() over a source that completed without emitting, so the 200 ms backoff never ran: ~54 kHz synchronous re-subscribe, inside the assertion's own .ToTask() and therefore out of reach of .Within(), [Fact(Timeout)], methodTimeout and the harness watchdog alike. 3 sites + UnthrottledResubscribeInTestRatchetGuard with a deliberately empty allow file.
    CrossHubPatchAtomicityTest #2339 #2391 — the write-identity gate serialised the entire node to read two scalars, once per pending patch per emission (9,455 full rebuilds on a 288-write burst). 🚨 Note the disposition, because it answers the warning at the end of the 13:04Z comment: this is a cost finding, not a MergeGuard logic defect. MergeGuard was measured refusing only the always-conflicting lastModified field, never a data key — so the "data loss on the cross-hub stream.Update path" concern did not materialise.

    The remaining two are owned, not orphaned. PartitionRootActivationTest and RoutingBackpressureShapeTest — the ones I flagged as unticketed in the 08:07Z and 09:48Z comments — are both already on #2346 ("shard-0 Orleans suite is red across unrelated PRs"), which is open and actively worked and which frames exactly the concentration those comments described. OrleansGrainTeardownStragglerTest (#2301) is closed; OrleansChatHistoryTest (#2291) is open and owned.

    🚨 Why this does not close

    Four merged root causes is not a measured rate. The fixes landed 14:30–14:46Z; roughly an hour of CI has run against them, which is nowhere near the 300-run window the 2026-08-13 sweep used, and a sweep taken now would overwhelmingly sample the pre-fix period. The number this issue was opened on — "just over half of all reds are a single unrelated test" — has not been re-taken, and it is the only thing that can close it.

    The closing criterion, concretely: re-run the 08-13 bucketing over a window that starts after 2026-08-26T14:46Z, once there are enough runs to be worth bucketing (a day or two). If the single-unrelated-test share has collapsed, this closes on that measurement. If it has not, the sweep names whatever replaced the six.

  16. rbuergi commented on Aug 26, 2026

    @rbuergi
    ContributorAuthor

    The measurement, re-taken — item 2 of this issue, on a fresh window

    Since the comment above said the rate had not been re-taken, here it is. Method matters because the previous sweeps under-counted for two specific reasons, both now covered:

    • Enumerated every MeshWeaver Build and Test run since 2026-08-21 (878) via the workflow-runs API, then pulled each failed shard job's own annotations (Test failed (shard N)). 🚨 The Test Results (shard N) reporter jobs are absent from many recent runs, so a sweep that reads the reporter silently misses those runs entirely.
    • 🚨 Re-run attempts are counted. 63 runs in the window have run_attempt > 1 and 34 now read success, hiding an earlier red — invisible to any run-level query. All figures below are attempt-level, recovered with ?filter=all.

    Window 2026-08-23T00:00Z → 2026-08-26T15:22Z (3.6 d, all branches)

    runs 613 across 192 branches · 653 attempts
    failure 139 runs (30.8 % of the 451 that reached success-or-failure); main push: 33 / 169 = 19.5 %
    failing attempts 178
    shape of the 178 count share
    exactly ONE distinct named test, nothing else failing 68 38.2 %
    one named test plus something else 38 21.3 %
    more than one named test 32 18.0 %
    host death, no test attributable 22 12.4 %
    non-test gate/build only 27 15.2 %

    So the shape this issue is named for is now 38 % of reds, not the ~60 % of "six of the last ten". It persists; it is no longer the majority.

    83 distinct tests failed, but 61 appeared on only ONE branch (96 occurrences) — those are overwhelmingly the PR's own fault. 13 tests appeared on ≥3 distinct branches, accounting for 117 occurrences and 49 of the 68 solo-cause attempts. That cross-branch set is the population, and distinct-branch-count is an objective proxy for "unrelated" that does not require reading each diff.

    The cross-branch population, and two things in it that are NOT flakes

    test occ branches solo dates
    Hosting.Test.LiveQueryHandoffDropTest 20 15 9 08-26 only
    AI.Test.AiContentPackDriftTest.EveryFile_StillHashesToItsPinnedReconciliationPoint 18 16 6 08-25 only
    Hosting.PostgreSql.Test.UserDirectoryCompletenessTests.AUserOutsideTheMostRecentlyModifiedPage_… 16 12 7 08-23→08-25
    Hosting.Monolith.Test.CrossProcessChangeFeedTest 11 10 6 08-24→08-26
    Hosting.Orleans.Test.OrleansMeshTests.HubWorksAfterDisposal(HubFactory) 10 7 5 08-24→08-26
    Hosting.Orleans.Test.PartitionRootActivationTest 9 9 2 08-24→08-26
    Hosting.Orleans.Test.OrleansChatHistoryTest.ColdStart_… 6 4 2 08-24→08-26
    Hosting.Monolith.Test.UserDirectoryContentShapeTest(typed) 5 3 3 08-24→08-25
    Hosting.Orleans.Test.OrleansGrainTeardownStragglerTest 5 4 1 08-25→08-26
    Hosting.Orleans.Test.OrleansMeshTests.HubWorksAfterDisposal(Kernel) 5 3 1 08-24→08-26
    Hosting.Orleans.Test.OrleansNodeChangePropagationTest.Delegation_NodeChanges_PropagateFromSubThread 5 4 1 08-25→08-26
    Hosting.Monolith.Test.PandasExplorerLayoutAreaTest 4 4 3 08-24→08-26

    🚨 The date column disqualifies the top two from this issue's taxonomy, and a raw occurrence count would have promoted both:

    Two members of the durable core are new to this issue and have no ticket I could find: UserDirectoryCompletenessTests (16 occ / 12 branches, 3 days) and OrleansNodeChangePropagationTest. The rest map onto #2346 / #2291 / the four fixed today.

    The failure mode this issue's taxonomy does not name at all

    22 of 178 failing attempts had a shard fail with ZERO test-name annotations — the host died, so no test can be blamed and no sweep that buckets by test name sees them:

    project classification occ branches first in window
    MeshWeaver.Threading.Test exit=124 TIMEOUT 12 8 (incl. main ×5) 2026-08-25T23:24
    Hosting.Monolith.Test (both halves) exit=124 TIMEOUT 7 5 (incl. main ×3) 2026-08-23T17:30
    Hosting.Orleans.Test exit=139 SIGSEGV 2 2 2026-08-24T09:47

    The Threading.Test row is exactly the population #2390 measured and fixed today (its own sweep: first 2026-08-25T23:24Z, 400 runs back to 08-22, none before) — independent corroboration from a different data path. The Hosting.Monolith.Test timeouts predate it and are still unattributed here.

    Not established, stated rather than glossed

    • 158 window runs concluded cancelled and were not inspected. A shard can fail and then be cancelled before the reporter runs, so every count above is a floor.
    • For the 22 host deaths the failing test is unknowable — no .trx was written. Only project + exit classification are established.
    • "Unrelated to the PR" is inferred from distinct-branch count, not from reading 178 diffs.

    Still open. The post-fix window is ~1 h old, so none of this measures whether today's four merges moved the rate; it measures the rate through them. Re-take it over a window starting after 2026-08-26T14:46Z in a day or two — that is the number that closes this.

  17. rbuergi commented on Aug 26, 2026

    @rbuergi
    ContributorAuthor

    Resolution status, 2026-08-26 end of day

    Six of the eight named members are closed, and — the part worth recording — every one turned out to be a real framework defect, not test flakiness.

    Issue Closed What it actually was
    #2377 ✅ Parameterless .ToObservable() binds CurrentThreadScheduler, whose [ThreadStatic] trampoline makes emissions queue instead of run when a Task resumes inline in an Rx pipeline. Fixed at 12 sites; cold-run failure rate 23% → 0/70. In production this silently empties a children listing — for ever.
    #2008 ✅ A per-node hub wrote its own activation seed back to storage; MonotonicWriteGuard then refused a legitimate write and AdoptDurableTruth rebased an owner that had never edited. The "1.3% flake" was which write won.
    #2341 ✅ .Catch(…).Repeat() where the source completed without emitting, so the backoff never ran — 460,000 re-subscribes in 8.45 s, synchronous, out of reach of every harness timeout.
    #2339 ✅ A cross-hub write path serialised the whole node per pending patch per emission — 9,455 → 0 rebuilds per burst.
    #2394 ✅ CancellationTokenSource.Cancel() runs callbacks synchronously on the caller's thread — and those callbacks are whole pooled-subscription teardowns. Mesh teardown ran application clean-up inline, unbounded and unlogged.
    #2305 ✅ The per-path write queue advanced on a local terminal, so write N+1 shipped a base N had replaced; the owner refused only the conflicting field and still acked Success.
    #2346 open shard-0 Orleans cluster — a 9.3 MB LOH grain-directory array per silo (194 live, 1.80 GB, 62% of heap) was removed, taking peak RSS 4.34 → 2.31 GB; deliberately Refs, not Closes, because it does not explain every cast member
    #2291 open narrowed to shape (B), a separate SharedOrleansFixture InvalidProgramException

    What this says about the issue's own thesis

    The premise — "unrelated single integration tests" — was right about the symptom and wrong about the cause. They were not unrelated. Three of the six are the same shape (an Rx pipeline that stalls, batches, or re-subscribes because of how a scheduler or a queue behaves under inline resumption), and several share a deeper one: a comment asserting a guarantee the code never delivered. #2305's write queue documented "the next queued Update sees post-patch state" with nothing implementing it; #2394's Drain was bounded by a timeout that covered only the step after the unbounded one.

    Also corrected during the sweep, and worth keeping: the earlier measurement here was understated, because gh run list reports only a run's latest attempt — every failure someone re-ran to green vanishes from the sample. Recovering attempts across 878 runs surfaced 34 hidden reds and moved "solo unrelated test" from ~60% to 38.2%. When surveying CI history, enumerate attempts, not runs.

    Remaining

    Two open members (#2346, #2291), plus #2421 (progress arriving as one batch — under measurement to split a real defect from a racy test) and #2436 (two sibling TOCTOU port helpers). Leaving this issue open until those land, since it is the population tracker and the population is not empty.

  18. rbuergi commented on Aug 28, 2026

    @rbuergi
    ContributorAuthor

    Two more data points from today (2026-08-28), both single unrelated tests

    Adding these here rather than filing separately — they are the population this issue is about, and one of them is a new member worth naming.

    1. MeshNodeLanguageServiceTest.ScriptCompletions_FilterByTypedPrefix_NotJustTheAlphabet — red on main at 12:32Z (run 33171489378, shard 2). Main was green immediately before (33170783055) and after (33172973648, 33173945145, 33174881868), on merges that do not touch the language service. Single test, single occurrence, no diff to blame.

    2. DataContextInitFaultedTest.FaultedInit_ReachesTerminalFailedState_AndAnswersFast — red on PR #2611's merge commit at 12:52Z (run 33172441705, shard 4). Filed separately as #2614 because it is new and therefore worth pinning while the trail is warm: the test landed on main the same day (5204e0fd4, #2528). Measured 25/25 green on origin/main locally under DOTNET_PROCESSOR_COUNT=4, and #2611's diff is confined to MeshWeaver.Graph's NodeType compilation while the test exercises MeshWeaver.Data's DataContext init — no path between them.

    What today suggests about the shape

    Both fit the profile in the issue body — a single unrelated integration test, on a shard, once — but neither is a live-model test. #2614's is a 15-second .Timeout on a path that should answer immediately (WithInitialData(() => Observable.Throw(...)) faults synchronously at subscribe). If that class of bound-under-load turns out to be a second generator alongside the live-model one, it wants a different remedy: raising it would be a band-aid, because the assertion exists precisely to catch a wedge, so a widened bound would hide the thing it was written to find.

    Cost this session: one full CI cycle on #2611, which is the tax this issue quantifies.

  19. rbuergi commented on Aug 29, 2026

    @rbuergi
    ContributorAuthor

    Fresh measurement — population still nonzero, ~1 flake-shaped red every ~12 h post-outage

    Same sweep as the sibling issues just closed/updated: every MeshWeaver Build and Test run on
    main, 2026-08-26T07:38Z → 2026-08-28T20:46Z (121 non-cancelled attempts, 19 failures), each
    failure read from its shard's own annotations.

    Excluding the known 08-26 morning merge-pile-up cluster (9 failures in 2.3 h, 07:58–10:11Z — a
    separate, already-discussed incident), the post-outage window is 2026-08-26T10:11Z →
    2026-08-28T20:46Z: 104 valid attempts, 11 failures
    :

    run when shape
    32956976911 08-26 10:11Z build break (Razor component conflicts)
    32991180742 08-26 16:55Z build break (MeshOperations not found)
    32994185103 08-26 17:26Z build break (same)
    32998090374 08-26 18:08Z build break (duplicate using MeshWeaver.AI)
    32999492758 08-26 18:24Z build break (missing Memex.AppHost import)
    33018632510 08-26 22:11Z singleton — OrleansChatHistoryTest.ColdStart_AgentSeesAllPreviousMessages
    33085734292 08-27 15:03Z Doc-content gate failure (different job)
    33101750253 08-27 18:05Z singleton — OrleansGrainTeardownStragglerTest
    33171489378 08-28 12:32Z singleton — MeshNodeLanguageServiceTest… (now #2616)
    33181004785 08-28 14:36Z singleton — DataContextInitFaultedTest… (now #2614)
    33202379023 08-28 19:06Z 2 named tests + HOST_CRASHED on shard 2

    5 of 11 are this issue's shape (a single unrelated integration test/host, nothing else on the
    shard) — roughly one every 12 h over the 58.5 h post-outage window. The population is not empty.
    Leaving open.

    One structural note for next time this is swept

    MeshWeaver.Plugins#786 ("The AI engine moves in", merged 2026-08-28T11:59:20Z) relocated the
    AI-specific Orleans tests (OrleansGrainTeardownStragglerTest, OrleansChatHistoryTest,
    OrleansNodeChangePropagationTest, OrleansSubThreadRoutingTest, OrleansAutoExecuteTest and
    siblings) out of this repo entirely, into MeshWeaver.Plugins/src/MeshWeaver.AI.Orleans.Test. Two
    of the five singletons above (ColdStart…, GrainTeardownStraggler…) are tests that can no
    longer fail in this repo's CI
    — closed #2500 today partly on that basis (#2346 stays open — it has outlived fixes before, see that thread). If this
    issue's population drops after 08-28T12:00Z, that migration is probably why; if a member with one
    of those names shows up again, it belongs in MeshWeaver.Plugins now, not here.

  20. rbuergi commented on Aug 29, 2026

    @rbuergi
    ContributorAuthor

    The sweep #1384 asked for: 1200 runs, every attempt, bucketed by test

    All numbers below are MEASURED from the GitHub Actions API unless a line says INFERRED.

    Method. All 1200 MeshWeaver Build and Test runs from 2026-08-22T12:40Z to 2026-08-29T07:41Z. For every run that failed or carries run_attempt > 1, I enumerated every attempt (376 run/attempt pairs) — a re-run-to-green otherwise hides the attempt that failed — pulled all 290 failed Run tests (shard N) job logs (48 MB) and extracted [FAIL] lines and the real [CI] <project> exit=N <TAG> result lines.

    Raw counts: 290 failed shard jobs · 371 (test, job) failure occurrences · 158 distinct test names.

    Run-level failure rate

    day runs failed rate
    08-22 33 4 12.1%
    08-23 35 5 14.3%
    08-24 131 25 19.1%
    08-25 239 54 22.6%
    08-26 404 108 26.7%
    08-27 184 32 17.4%
    08-28 136 18 13.2%
    08-29 (part) 38 1 2.6%

    The issue's "every second PR" was written at the 08-26 peak. It has since roughly halved.

    🚨 The headline: a third of the population is victims, not flakes

    This is the finding that changes how the list should be read.

    10 cascade events produced 67 of 371 occurrences (18.1%) and touched 50 of 158 distinct test names (31.6%).

    cascade events occurrences distinct names blamed
    #890 Roslyn-emit NRE poisons the process 9 54 (14.6%) 37
    #2291-(B) Orleans fixture InvalidProgramException 1 13 (3.5%) 13

    A single #890 event takes down 2–10 test names at once (max 10, on fix/install-seed-cap-outruns-its-fallback 08-27). Those 37 names include CodeEditRecompileTest ×6, NodeTypeReleaseGateTest ×3, NodeTypeCompileParkTest ×3, CompileLeafStabilityTest ×2, LinkedInTelemetryImportTest, PandasExplorerLayoutAreaTest, StaleStampRootBindingTest… i.e. exactly the "unrelated single integration test" texture the issue describes. Each one costs a fresh attribution investigation, and the answer is always the same. This is a ~6× blame amplifier: 9 real events wearing 37 names.

    Same shape as this issue's own #2291 finding, at scale. INFERRED: a meaningful share of the "six of the last ten" that prompted this issue were cascade victims rather than distinct flakes.

    Ranking by frequency

    # occ branches last seen test
    1 22 19 08-26 AI.Test.AiContentPackDriftTest.EveryFile_StillHashesToItsPinnedReconciliationPoint
    2 20 15 08-26 Hosting.Test.LiveQueryHandoffDropTest.A_notification_snapshotted_before_Initial_…
    3 17 13 08-25 PostgreSql.Test.UserDirectoryCompletenessTests.AUserOutsideTheMostRecentlyModifiedPage_…
    4 14 14 08-27 Orleans.Test.PartitionRootActivationTest.BarePartitionPath_NoMeshNode_RespondsToPing
    5 11 9 08-27 Orleans.Test.OrleansGrainTeardownStragglerTest.GrainLifetimeCallbacks_AfterActivationIsInvalid_…
    6 11 10 08-26 Monolith.Test.CrossProcessChangeFeedTest.AWriteInOneProcess_ReachesTheOtherProcessesLiveMirror_…
    7 9 6 08-27 Orleans.Test.OrleansChatHistoryTest.ColdStart_AgentSeesAllPreviousMessages (#2291)
    8 9 4 08-27 AI.Test.AgentToolCancellationTest.GetVersions_ParkedOnAStalledStore_UnwindsWhenTheRoundIsCancelled
    9 9 8 08-28 Orleans.Test.OrleansNodeChangePropagationTest.Delegation_NodeChanges_PropagateFromSubThread
    10 7 3 08-29 FutuRe.Test.FutuReAnalysisTest.EuropeRe_AnnualReport_EmbeddedCharts_ShouldRenderViaPathResolution
    11 6 6 08-27 Monolith.Test.PandasExplorerLayoutAreaTest.Explorer_NoPythonNode_RendersToolbarAndDegrades…
    12–18 5 each 1 08-26 MemexTemplate.Test.MemexTemplateGeneratorTest.* (7 tests, one branch — not a flake, a broken branch)
    19 4 4 08-28 Monolith.Test.MeshNodeLanguageServiceTest.ScriptCompletions_FilterByTypedPrefix_… (#2616)

    Distinct-branch count is the flake discriminator, not occurrence count. Rows 12–18 fail 5× each on one branch — that is a broken branch, correctly reported. Row 1 fails on 19 branches — that is a ledger/gate problem hitting everyone.

    ⚠️ The top of the list is NOT where the remaining tax is

    Ranks 2, 4 and 6 already have landed fixes on origin/main (verified by grepping the code onto main, not by PR title):

    So the ranking is dominated by bursts that were found and fixed. Ranking on the full window over-weights them. The list that actually costs re-runs now is:

    Still failing on/after 08-27:

    occ since 08-27 branches test
    7 3 FutuReAnalysisTest.EuropeRe_AnnualReport_EmbeddedCharts_ShouldRenderViaPathResolution
    6 9 OrleansGrainTeardownStragglerTest.GrainLifetimeCallbacks_AfterActivationIsInvalid_…
    4 4 MeshNodeLanguageServiceTest.ScriptCompletions_FilterByTypedPrefix_… (#2616)
    3 4 AgentToolCancellationTest.GetVersions_ParkedOnAStalledStore_…
    3 14 PartitionRootActivationTest.BarePartitionPath_…
    3 3 DataContextInitFaultedTest.FaultedInit_ReachesTerminalFailedState_AndAnswersFast
    2 8 OrleansNodeChangePropagationTest.Delegation_NodeChanges_PropagateFromSubThread

    Exit-code census (deduped per job)

    count signature
    243 exit=1 TESTFAIL
    30 exit=1 MASKED (host died after streaming results)
    22 exit=124 TIMEOUT — 12 Threading.Test, 9 Hosting.Monolith.Test (part N/2), 1 PluginCatalog.Test
    4 exit=139 SIGNAL (#613) — 2 Orleans, 2 FutuRe

    ⚠️ Correction to a trap in this issue's own text. #1384 says #890 "accounts for essentially the entire Hosting.Monolith.Test timeout population". My first pass appeared to refute that — I grepped MeshWeaver.Hosting.Monolith.Test exit=124 and got zero. That was my bug: since sharding, the label is MeshWeaver.Hosting.Monolith.Test (part 2/2), so the exact-string grep silently returned nothing. Corrected count is 9. Anyone re-running this sweep must match the (part N/M) suffix or they will conclude a live population is extinct.

    Also worth naming: the single largest timeout bucket is MeshWeaver.Threading.Test (12), not Monolith — a burst confined to 08-25/08-26, entirely on shard 5, zero since. Not #890 (different project, no NRE in those logs). Unattributed; I am not filing it as it appears self-resolved, but if Threading.Test shard-5 timeouts return, this is the prior.

    What I recommend follows from the numbers

    1. Fix CI: one Roslyn Emit poisons the whole process — every later compile NREs in Cci.MetadataWriter.GetConsolidatedTypeParameters, shard ends exit=124 #890 and you remove ~15% of all occurrences and ~23% of distinct names at one stroke — far more than any single test on the ranking. It is the highest-yield item and its instrument has now spoken; see my comment there.
    2. Make cascades attributable. Both CI: one Roslyn Emit poisons the whole process — every later compile NREs in Cci.MetadataWriter.GetConsolidatedTypeParameters, shard ends exit=124 #890 and Flake: OrleansChatHistoryTest.ColdStart_AgentSeesAllPreviousMessages — TWO different failures share this name (a Completed cell stuck on 'Generating response...', and a fixture deploy that took 13 tests down) #2291-(B) manufacture unrelated-looking failures because a process-wide/fixture-wide fault is reported once per victim test. Until that is fixed, every flake ranking anyone builds — including this one — is polluted, and every triage of a victim is wasted.
    3. Rank on distinct branches, not occurrences, and window to the last ~48h. The full-window ranking is a history of already-fixed bursts.

    Data and scripts are reproducible from the Actions API; method is described above so anyone can re-run it.

  21. rbuergi commented on Aug 29, 2026

    @rbuergi
    ContributorAuthor

    Addendum: the cross-repo entry, and the theme that runs through all of it

    Module bundle (MeshWeaver.Markdown.Export) — RESOLVED, and it was never a flake

    Folding in the one the ranking above does not cover (it lives in Plugins' Plugin Catalog CI, not this workflow). Two occurrences on two unrelated PRs — Plugins #854 (a MeshWeaver.Hosting.PostgreSql-only change) and #860 — both on PixelFidelityOfferedOnlyWhenAvailableTest.MarkdownExportDialog_NeverOffersPixelFidelity, 60 s observable timeout against an 864 ms – 1.06 s local runtime.

    Two hypotheses were on the table: runner starvation in the 40-way matrix, or a genuine race. It was the race, and it was a product bug. ExportDocumentLayoutArea.RenderExport did Post(...) and then hub.Observe(delivery).Subscribe(...) — the overload that registers the response subject after the request is in flight. An unmatched correlation id is dropped (HandleCallbacks → RESPONSE_ARRIVED_NO_SUBJECT → delivery.Processed()), so the enriched control never arrives and the dialog stays on its seed forever. The request targets the hub's own address, the shortest and most often synchronous response path there is.

    The discriminator was inside the failing class itself, and it is worth reusing: the sibling test waits for a value the seed already has, so a lost reply cannot fail it; the failing test waits for NodeName, which only the reply can supply. Starvation would have hit both. Only one failed. That asymmetry ruled out hypothesis 1 without needing a green-on-main control — which, as noted, does not exist for that job.

    Fixed and merged as Plugins #872, with a ratchet guard that reds on the reverted source. Real users could hit the same lost reply.

    The theme: four of the five things I looked at were diagnostics that could not be read

    This is the part I would carry forward from the whole sweep, because it is why the flake population is expensive out of proportion to its size.

    the instrument why it could not be read
    #890 the emit canary its "pristine" control shared CoreLib's file mapping with the set it was exonerating, so BELOW-ROSLYN was unearned — 9 of 9 verdicts (PR #2663)
    #2616 IoPoolDrain: pool '{PoolName}' … written to ILogger after Mesh.Dispose(); 0 of 294 dispose windows carry any ILogger line (PR #2665)
    #2291-(B) a fixture bring-up fault reported under 13 other tests' names
    #890 (again) "Roslyn is already serialized on the Compile IoPool" asserted twice in DynamicTypePreWarmer, false, and it retired a live axis without testing it

    Plus the one I introduced myself and had to correct: grepping Hosting.Monolith.Test exit=124 returned zero because sharding renamed the label to (part N/M), which would have "proved" a live population extinct.

    Every one of these makes a real defect look like a flake, or a flake look like nothing at all. That is the mechanism behind this issue's central complaint — the attribution cost, not the failure count. My concrete recommendation for #1384 stands and sharpens:

    1. Cascades must fail as themselves. 10 events wearing 50 distinct test names is 31.6% of the population, and every victim costs a fresh, always-identical investigation.
    2. A diagnostic that runs during teardown may not use ILogger. It is structurally unreadable there. This is a general rule, not a MeshNodeLanguageServiceTest teardown DIRTY — one pooled I/O leaf still ignores cancellation after #2598 (#2665 names the pool on the next occurrence) #2616 detail.
    3. A control is only a control for what it does not share, and a probe must not answer its most expensive branch on evidence it never checked.

    Status of what I touched

    issue outcome
    #1384 ranking + method posted above; this addendum closes the cross-repo gap
    #890 canary verdict is unearned → PR #2663 (open). Root cause NOT fixed; likely the same upstream CoreCLR fault as #613
    #2616 premise refuted (predates #2598 by 4 h; the test PASSED — teardown failed) → PR #2665 (open)
    #2291 (A) fixed by 3 commits as of 08-27 07:51Z; (C) is a new third shape with genuine history loss; (B) handed to the #2638/#2635 disposal owner
    Plugins export dialog merged (Plugins #872)

    ⚠️ Not a flake finding but the most important thing I hit: src/MeshWeaver.AI.Orleans.Test (~28 files, ~7,200 lines) runs in no CI job at all since 2026-08-28, though ai-engine/SKILL.md:26 claims it runs in portal-hosts. "No failures since 08-26" for anything in that suite is not evidence of health. Details on #2291.

  22. rbuergi commented on Aug 29, 2026

    @rbuergi
    ContributorAuthor

    The Monolith.Test host CRASHES are a distinct population member, and two of four name one test

    Four MeshWeaver.Hosting.Monolith.Test host deaths today, on diffs that cannot reach the host (a concurrency: block, a Python script, workflow/script-only changes). The classifier reports MASKED: host crashed AFTER streaming results — the trx records fewer failures than the exit code implies, so the process died during or after teardown having already reported.

    Two of the four name the same test, ~2 h apart, on core #2690 (run 33268909413) and #2693 (run 33274198115):

    SilentReadNackTest.OwnerDisposedWithReadOutstanding_IsNacked_NotLeftHanging  [FAIL] + host exited non-zero
    

    Why that name is worth taking literally

    The test's own remarks describe a mechanism that is hard to distinguish from the crash:

    "disposing the owner's MeshNode data-source stream makes every later read of that hub permanently unanswerable (the data source hands the disposed stream back on every GetStreamForPartition; there is no liveness check there), so the read is guaranteed to be outstanding."

    So the test deliberately leaves a read outstanding against an already-disposed stream, then tears the owner down. That is the exact shape of a use-after-dispose — and the hypothesis worth testing is that the scenario escapes its own scope rather than an unrelated straggler killing the shard.

    What rules out the obvious explanation

    MonolithMeshTestBase already joins the disposal signal — WaitWithProgressAsync awaits Mesh.DisposalCompleted (bounded, ConfigureAwait(false)), then IoPoolRegistry.DrainAll() cancels and joins offloaded I/O, then AsyncDisposeQueue.DrainAsync. So "the test base does not wait for disposal" is disproven; whatever survives is surviving past a join that exists.

    The teardown's own comment says why that is possible: DisposalCompleted covers the hub's action blocks and message round-trips, and explicitly not ThreadPool leaves. A read parked against a disposed data-source stream is neither an action block nor an IIoPool leaf, so it may be covered by neither.

    The cheap next step

    TestPhaseTrace is written for exactly this: the post-dispose phases are traced individually (DISPOSE_IOPOOL_DRAIN_START/DONE with pool names and residual counts, DISPOSE_ASYNC_QUEUE_DRAINED clean=…), and the comments note it is all a killed host leaves behind — the mesh's log sink stops capturing at Mesh.Dispose(), so ILogger output is structurally absent. Reading that file from either crashed run should say which phase the process died in, and a named residual pool would distinguish "a leaf we know about" from "the outstanding read".

    Recording rather than chasing: this is core's, and the two runs above are the evidence to start from. Flagging it here because a host crash is materially worse than a flaky assertion — it kills the shard and masks every other result in it, so it is the population member most likely to be hiding others.

    Separately: the ~30 s assertion flakes in this population are one root, filed as #2700 (a hand-written 30 s bound repeated ~2,679 times). These crashes are not that — different mechanism, and they should not be folded into it.

  23. rbuergi commented on Aug 30, 2026

    @rbuergi
    ContributorAuthor

    Another data point (2026-08-30, PR #2807 — a docs-only diff, three markdown files): run 33322993649, shard 1, MeshWeaver.Hosting.Monolith.Test (part 1/2) exit=124 TIMEOUT (8m wall-clock cap hit). Not a hang: the host started 16:41:21Z and tests were still STARTING at 16:48:52Z (CodeEditRecompileTest.CompileTool_UnchangedSources_ForcesFreshRun…) when the cap fell at 16:49:21Z — 184 class args in that part, and the main runs before and after it were green. Capacity on the shard, not a test defect; the diff cannot reach the project.

  24. rbuergi commented on Aug 30, 2026

    @rbuergi
    ContributorAuthor

    Correction to my comment above: the sweep session identified this as #2747 — Hosting.Monolith.Test split in 2 puts each half at ~332 s against the 480 s per-project cap (69%), so a slightly slower shard tips it over; PR #2800 splits it into 3 and adds a headroom guard. Not a capacity flake in the abstract — a known long pole with no headroom check.

  25. rbuergi commented on Aug 30, 2026

    @rbuergi
    ContributorAuthor

    Closing: both halves of this population were answered, and one of them left the repo

    This issue asked for three things. Measured on today's main:

    1. "Decide whether live-model assertions belong in the gating suite at all" — moot in this repo

    Both named carriers are gone from core:

    DelegationSubThreadUsageTest      core: (absent)   plugins: src/MeshWeaver.AI.Test/…
    OrleansSubThreadAutoResumeTest    core: (absent)   plugins: src/MeshWeaver.AI.Orleans.Test/OrleansSubThreadAutoResumeTest.cs
    ls test/ | grep -c '^MeshWeaver\.AI'  →  0
    

    The AI suite moved to MeshWeaver.Plugins with the engine (#2276). The one core survivor is MeshWeaver.Portal.E2E.Test/ChatDelegationTest.cs, which is an e2e test and not part of the gating unit suite this issue was about. So the question — "can an assertion whose outcome depends on an external API's latency and chunking gate a merge?" — is now a MeshWeaver.Plugins question and should be asked there if it still bites.

    2. "Quantify the rest" — done, and it produced named issues rather than a vague population

    This was the substantive ask, and it was carried out at length:

    3. "Do not fix any of these by raising a timeout or a retry count" — held, and vindicated

    Worth recording, because it was tested this evening and nearly failed. A 6× teardown regression in one project presented as four failing tests with a rotating cast, all green in isolation. Raising the enclosing timeout would have turned the suite green and shipped it. What found it was timing the whole project against main, one variable at a time. The rule in this issue is why that was not done.

    Why close rather than keep as an umbrella

    Its own diagnosis — "these are not three coincidences, it is a population" — was right, and the value was in converting the population into attributable, individually-actionable issues. That has happened. What remains open (#890, #2700) are those issues, each with measurements this one could not have carried. Keeping a bucket open above them adds a second place to look without adding information.

    Reopen if a new unattributed population appears — the technique recorded here (bucket every failed shard over a window, rank by test) is the reusable artifact and it worked twice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions