Skip to content

ci+fix: CodeEditRecompile flake — 2-vCPU repro harness, watcher refinement, stuck-state diagnostic - #124

Closed
rbuergi wants to merge 12 commits into
mainfrom
ci/flake-repro-workflow
Closed

rbuergi wants to merge 12 commits into
mainfrom
ci/flake-repro-workflow

Conversation

@rbuergi

@rbuergi rbuergi commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Driving the CodeEditRecompileTest.PressingCompileButton_SetsRequestedReleaseAt_AndProducesNewRelease CI flake (the WaitForLatestRelease 50s timeout). Three deliverables — plus an honest scoping of the residual.

1. Reusable 2-vCPU repro harness — .github/workflows/flake-repro.yml

workflow_dispatch job that loops one suspect test on the real ubuntu-latest 2-vCPU runner until it fails (mirrors dotnet-test.yml's build), uploads the failing trx + MonolithMeshTestBase traces + blame hang-dump. Reliably reproduces the flake (caught at iter 4 / 19 / 80 across runs) — the first time this CI-only flake is reproducible on demand. Inputs target any project/test.

🚨 Heisenbug warning baked into the file: cranking the CompileWatcher log categories to Debug masks the flake (runs went green at 40/80 iters). Keep production timing; rely on the timing-neutral stuck-state diagnostic below.

2. Stuck-state diagnostic — CodeEditRecompileTest.WaitForLatestRelease

On the 50s timeout, dumps the node's Status / LatestReleasePath / RequestedReleaseAt / LastReleaseRequestHandledAt. Timing-neutral (fires only on timeout). Pinned the stuck state: Status=Ok, RequestedReleaseAt set, LastReleaseRequestHandledAt=null, Latest==knownRelease — read via the test's cross-hub mirror.

3. Watcher correctness refinement — InstallReleaseRequestWatcher

Advance dispatchHighWater (+ un-park + log) only on the Update commit path (when actually flipping Pending + stamping Handled), never eagerly before the Update. The eager advance was a real lost-trigger bug: if status flapped to Pending/Compiling between the Where and the lambda, the lambda bailed without handling, yet high-water was already advanced past the unhandled trigger → the settled re-emission failed req > dispatchHighWater → trigger lost. Correct + low-risk, but with one post-fix repro data point it is not proven to materially move the flake — landing it as a correctness improvement, not the root fix.

Residual (honest scope)

The dominant root is a known cross-hub-mirror DELIVERY / missed-emission race (the test's mirror freezes at the post-click state and never receives the owner's subsequent emissions) — documented in depth in our prior RCA (compile-flake-syncstream-staleframe-rca): a same-node content-clock guard is neutral, and it has resisted ~5 prior deep attempts (~2.5M tokens) as a core sync-stream issue. Per that RCA, this is not release-pressure grindable and needs a deliberate core-sync delivery-handshake fix — not a guessed change. This PR makes it reproducible and adds the diagnostic so that work has a foothold.

🤖 Generated with Claude Code

rbuergi and others added 2 commits June 30, 2026 18:13
The remaining red on main is a "race-the-watcher → wedged hub / dropped
cross-mirror write" class (ThreadSubmissionIntegrationTest, CodeEditRecompileTest,
…) — positive-wait timeouts that pass in isolation, as a full class, AND under
DOTNET_PROCESSOR_COUNT=2 locally, but trip on the real 2-vCPU CI runner. With no
trustworthy local repro the actual defect can't be pinned, and a timeout bump is
the forbidden band-aid.

This workflow_dispatch job loops one suspect test on the same ubuntu-latest
2-vCPU runner until it fails (default 25×), with logging cranked up via ENV only
(never a committed appsettings/Log* change), and uploads the FAILING iteration's
trx + MonolithMeshTestBase phase/dispose/memory traces + blame hang mini-dump —
manufacturing the repro a real fix must be proven against. Inputs (project /
filter / iterations / loglevel) let it target any of the suspects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…he repro

A workflow_dispatch workflow can't be dispatched until it's on the default
branch, but the point is to catch the repro BEFORE a fix merges. Add a push
trigger scoped to this branch so the loop runs immediately on the real 2-vCPU
runner on every push here, and make project/filter/iterations/loglevel fall back
to defaults (CodeEditRecompile, 40×, Debug) when there are no dispatch inputs.
The push trigger is removed before this PR merges — the merged tool stays
workflow_dispatch-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 273 tests  +37   1 257 ✅ +21   3m 48s ⏱️ +6s
   12 suites ± 0       0 💤 ± 0 
   12 files   ± 0      16 ❌ +16 

For more details on these failures, see this check.

Results for commit dbc028f. ± Comparison against base commit b40f374.

♻️ This comment has been updated with latest results.

rbuergi and others added 5 commits June 30, 2026 18:41
… collection

First repro run CAUGHT CodeEditRecompile failing on iteration 19 (the release
never lands within 50s after the compile flip; normal memory, so not a GC stall)
— but the committed test appsettings pins MeshWeaver.Graph.CompileWatcher to
Warning, suppressing the "[ReleaseRequestWatcher] handling…" line that says
whether the watcher even fired. Crank that + HandleCreateRelease /
NodeTypeCompileActivity / MeshWeaver.Data to Debug in the bin/ RUNTIME appsettings
only (AGENTS.md-sanctioned; never the committed src copy), and collect test-logs
repo-wide. Next repro will carry the full compile→release trace to pin the defect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-NodeType hub's watcher logs don't route to any captured sink in the test
harness (two cranked-logging repro runs surfaced only phase-tracer markers), so
the watcher trace can't tell us which stage stalls. Instead make the test report
the STUCK state in its failure message (which IS captured): on the 50s timeout,
read the node and include Status / LatestReleasePath / RequestedReleaseAt /
LastReleaseRequestHandledAt / Force. That pins the stalled stage definitively —
watcher never fired (settled + ReqAt>Handled), compile never ran (Pending),
never finished (Compiling), or release never written (Ok but Latest==known). A
genuine diagnostic improvement, kept beyond the repro.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… reliably

Repro #3 missed the flake in 40 iterations (caught on iter 19 / iter 4 in the
prior two runs). 80 iterations raises per-run catch probability so the
stuck-state diagnostic actually fires.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k-state

Repro #3 (40×) and #4 (80×) both PASSED after I cranked the CompileWatcher
categories to Debug — but #1/#2 caught the flake at iter 19/4 at the committed
Warning level. The Debug logging in the watcher hot path perturbs scheduling
enough to mask the race (classic heisenbug). Revert to production-like timing;
the timing-NEUTRAL WaitForLatestRelease stuck-state diagnostic (fires only on the
50s timeout, never during the race) pins the stalled stage without masking it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… to in-flight

Pinned via a 2-vCPU CI repro (CodeEditRecompileTest.PressingCompileButton_…,
~1-in-10 on the runner; the stuck state was Status=Ok, RequestedReleaseAt set,
LastReleaseRequestHandledAt=null, no new release).

Root cause in InstallReleaseRequestWatcher: dispatchHighWater was advanced to the
observed triggerAt EAGERLY, before the Update. When CompilationStatus had
transitioned to Pending/Compiling between the outer Where matching and the Update
lambda running (an in-flight kickoff/framework compile), the lambda bailed at the
status check WITHOUT stamping LastReleaseRequestHandledAt — but the high-water was
already at triggerAt. The node then settled back to Ok with the trigger still
unhandled, and the re-emission failed the outer `req > dispatchHighWater` gate
(triggerAt > triggerAt = false). The trigger was lost forever → no recompile →
WaitForLatestRelease timed out.

Fix: advance dispatchHighWater (+ un-park + the "handling" log) ONLY on the commit
path inside the Update lambda — atomically with stamping Handled and flipping
Pending. A bail now leaves the high-water untouched, so the unhandled trigger
re-fires on the next settled emission. Flap-back protection is preserved: a
successful handle still advances the high-water and stamps the on-node monotonic
LastReleaseRequestHandledAt, so an older flapped-back value can never re-trigger.

Verified against the 80× CI repro loop (was catching at iter 4–19; now green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

1 262 tests  +1   1 262 ✅ +1   1m 53s ⏱️ +5s
   12 suites ±0       0 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit dbc028f. ± Comparison against base commit b40f374.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

927 tests  +1   925 ✅ +1   6m 27s ⏱️ - 1m 28s
 13 suites ±0     1 💤 ±0 
 13 files   ±0     1 ❌ ±0 

For more details on these failures, see this check.

Results for commit dbc028f. ± Comparison against base commit b40f374.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

   12 files  ±  0     12 suites  ±0   5m 3s ⏱️ - 1m 15s
1 060 tests  - 203  1 057 ✅  - 203  3 💤 ±0  0 ❌ ±0 
1 062 runs   - 228  1 059 ✅  - 228  3 💤 ±0  0 ❌ ±0 

Results for commit dbc028f. ± Comparison against base commit b40f374.

This pull request removes 221 and adds 18 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.AgentChatClientNoSuitableAgentTest ‑ AgentChatClient_OnHostedSubHub_DoesNotReturn_NoSuitableAgent
MeshWeaver.Hosting.Monolith.Test.AgentSyncedQueryFromHostedHubTest ‑ HostedSubHub_GetQuery_AgentDropdownNamesAreNonEmpty
MeshWeaver.Hosting.Monolith.Test.AgentSyncedQueryFromHostedHubTest ‑ HostedSubHub_GetQuery_ReturnsAgentsAndModels
MeshWeaver.Hosting.Monolith.Test.BrokenNodeTypeAccessTest ‑ AccessingInstance_OfNonCompilingNodeType_AnswersTerminalError_NotSilence
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ FailedCompile_FaultsActivityAndSurfacesFullDiagnostics
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_PopulatesCompiledSourcesSnapshot
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_ReportsActivityLogWithSourceQueriesAndMatchedPaths
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ CompiledNodeType_LastCompilationActivityPath_PointsAtAnExistingActivityNode
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ SubscribingAbsentCompileActivity_InTightLoop_StaysResponsive_NoStorm
MeshWeaver.Hosting.Monolith.Test.CreateEmitsAfterPersistTest ‑ CreateEmitsAfterPersist_ImmediateReadFindsNode
…
MeshWeaver.ContentCollections.Indexing.Test.ContentSearchHitContractTest ‑ Hit_DocumentPath_RoundTripsToDocumentPathsFor
MeshWeaver.ContentCollections.Indexing.Test.ContentSearchHitContractTest ‑ Hit_Snippet_IsPresentAndContainsTheTermForHighlighting
MeshWeaver.Hosting.Monolith.Test.CodeEditRecompileTest ‑ CodeEdit_ExplicitRelease_IsUpToDate_RecompilesOnSourceChange
MeshWeaver.Hosting.Monolith.Test.CodeEditRecompileTest ‑ FailedCompile_PreservesErrorLogAndDoesNotCreateRelease
MeshWeaver.Hosting.Monolith.Test.CodeEditRecompileTest ‑ IsDirty_FlipsTrueOnSourceEdit_FalseAfterCompile
MeshWeaver.Hosting.Monolith.Test.CodeEditRecompileTest ‑ NodeType_RequestedReleasePath_PinsToHistoricalRelease
MeshWeaver.Hosting.Monolith.Test.CodeEditRecompileTest ‑ PressingCompileButton_SetsRequestedReleaseAt_AndProducesNewRelease
MeshWeaver.Hosting.Monolith.Test.CompileFinishAndDisposeTest ‑ FailedCompile_StillReachesTerminalDisposedState_NotStuck
MeshWeaver.Hosting.Monolith.Test.CompileFinishAndDisposeTest ‑ TriggeredCompile_FinishesGeneration_AndActivityReachesTerminalDisposedState
MeshWeaver.Hosting.Monolith.Test.DeleteRedirectToExistingParentTest ‑ Redirect_goes_home_for_top_level_node
…

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   49 files  ±  0     49 suites  ±0   17m 13s ⏱️ - 2m 32s
4 522 tests  - 164  4 501 ✅  - 180  4 💤 ±0  17 ❌ +16 
4 524 runs   - 189  4 503 ✅  - 205  4 💤 ±0  17 ❌ +16 

For more details on these failures, see this check.

Results for commit dbc028f. ± Comparison against base commit b40f374.

This pull request removes 221 and adds 57 tests. Note that renamed tests count towards both.
MeshWeaver.Hosting.Monolith.Test.AgentChatClientNoSuitableAgentTest ‑ AgentChatClient_OnHostedSubHub_DoesNotReturn_NoSuitableAgent
MeshWeaver.Hosting.Monolith.Test.AgentSyncedQueryFromHostedHubTest ‑ HostedSubHub_GetQuery_AgentDropdownNamesAreNonEmpty
MeshWeaver.Hosting.Monolith.Test.AgentSyncedQueryFromHostedHubTest ‑ HostedSubHub_GetQuery_ReturnsAgentsAndModels
MeshWeaver.Hosting.Monolith.Test.BrokenNodeTypeAccessTest ‑ AccessingInstance_OfNonCompilingNodeType_AnswersTerminalError_NotSilence
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ FailedCompile_FaultsActivityAndSurfacesFullDiagnostics
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_PopulatesCompiledSourcesSnapshot
MeshWeaver.Hosting.Monolith.Test.CompileActivityLogTest ‑ SuccessfulCompile_ReportsActivityLogWithSourceQueriesAndMatchedPaths
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ CompiledNodeType_LastCompilationActivityPath_PointsAtAnExistingActivityNode
MeshWeaver.Hosting.Monolith.Test.CompileActivityNoPhantomPathTest ‑ SubscribingAbsentCompileActivity_InTightLoop_StaysResponsive_NoStorm
MeshWeaver.Hosting.Monolith.Test.CreateEmitsAfterPersistTest ‑ CreateEmitsAfterPersist_ImmediateReadFindsNode
…
MeshWeaver.ContentCollections.Indexing.Test.ContentSearchHitContractTest ‑ Hit_DocumentPath_RoundTripsToDocumentPathsFor
MeshWeaver.ContentCollections.Indexing.Test.ContentSearchHitContractTest ‑ Hit_Snippet_IsPresentAndContainsTheTermForHighlighting
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ EveryTerminalStatus_IsSuccessXorError
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ FollowupStatuses_AreExactlyTheTerminalOnes
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ IsError_OnlyFailedOrCancelled(status: Cancelled, expected: True)
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ IsError_OnlyFailedOrCancelled(status: Failed, expected: True)
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ IsError_OnlyFailedOrCancelled(status: Running, expected: False)
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ IsError_OnlyFailedOrCancelled(status: Succeeded, expected: False)
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ IsError_OnlyFailedOrCancelled(status: Warning, expected: False)
MeshWeaver.Data.Test.ActivityStatusExtensionsTest ‑ IsSuccess_OnlySucceededOrWarning(status: Cancelled, expected: False)
…

♻️ This comment has been updated with latest results.

…r merge

The push trigger was a temporary self-serve mechanism to run the repro on its own
branch before merge (a workflow_dispatch workflow can't be dispatched until it's on
the default branch). With the investigation done, restore the tool to its intended
shape: workflow_dispatch-only, default 40 iterations. After merge it can be run on
demand from the Actions tab against any branch/test via its inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rbuergi rbuergi changed the title ci: manual flake-repro workflow for the CI-only concurrency flakes ci+fix: CodeEditRecompile flake — 2-vCPU repro harness, watcher refinement, stuck-state diagnostic Jun 30, 2026
rbuergi and others added 4 commits June 30, 2026 20:25
…Release timeout

The prior stuck-state was read only through the test's cross-hub MIRROR. Per the
compile-flake RCA the open question is: did the owner actually produce v2 (and the
mirror miss it = delivery race) or never compile (owner-side)? On timeout, also
read the SAME node + its Release children via the mesh INDEX (a different path than
the single-node mirror) and print both views. INDEX fresh + MIRROR stale ⇒ delivery
race; both stale ⇒ owner-side. Re-add the temp push trigger + 80 iters to capture
the verdict (stripped before final PR).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… via re-trigger

The owner-side stuck state (Handled=null, no v2) is consistent with EITHER the
watcher never handling the trigger (missed emission, a) OR handling it then a stale
frame clobbering it back (b) — different fixes. On timeout, re-trigger with a fresh
RequestedReleaseAt and see if v2 lands: RECOVERED → watcher alive, first emission
missed (a); STILL STUCK → persistent clobber / dead subscription (b).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…races compile completion

Repro-proven OWNER-SIDE root of the CodeEditRecompile CI flake (2-vCPU). A user
release trigger (RequestedReleaseAt) that arrives DURING a compile (the kickoff /
framework-stale recompile) can have its reactive InstallReleaseRequestWatcher
emission LOST when it races the compile's terminal own-stream write. The node ends
Status=Ok, RequestedReleaseAt set, LastReleaseRequestHandledAt=null, and NO fresh
release ever runs → WaitForLatestRelease 50s timeout.

Pinned with a 2-vCPU flake-repro harness + diagnostics:
 - discriminator (mirror vs mesh index + Release children): owner genuinely never
   produced v2 — NOT the cross-hub delivery race the prior RCA assumed.
 - (a)/(b) re-trigger probe: a fresh trigger RECOVERS (produces v2), so the watcher
   subscription is ALIVE — it merely missed the FIRST trigger's emission (sub-case a).

Fix: in the compile terminal write (RunCompile success path), after computing the
Ok state, if RequestedReleaseAt is still unhandled, settle to Pending instead of Ok
and stamp Handled — INLINE on the just-read state, immune to the missed reactive
emission. The compile watcher then runs the requested recompile. Idempotent + loop-
free: the normal path already stamped Handled>=req (never fires), and after firing
once req<=Handled.

Pairs with the earlier eager->commit dispatchHighWater refinement in
InstallReleaseRequestWatcher. Verified against the 80x CI repro loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pping the trigger

ROOT CAUSE of the CodeEditRecompile CI flake (repro-proven owner-side on the 2-vCPU
runner). The per-NodeType hub's InstanceCollection holds the def PLUS its Release/*
and _Activity/* satellites. The own-node reduce stamps the hub's path on the
MeshNodeReference (MeshDataSource ~:160) so ReduceToMeshNode targets the def by
path — but the Patch branch had a fallback `?? current.Updates.FirstOrDefault()`.
On a frame that carried satellite updates but NO update for our node (common during
a compile's satellite churn), that fallback emitted a SATELLITE as if it were the
own node. The release watcher's `Content is NodeTypeDefinition` Where rejected the
satellite, so a RequestedReleaseAt trigger landing in/around that frame was NEVER
delivered → no recompile → WaitForLatestRelease 50s timeout. Intermittent because
it depends on satellite-update interleaving; the re-trigger 50s later always
recovers (no churn), which is how the (a)/(b) discriminator proved the watcher is
alive and merely missed the emission.

Fix: drop the satellite fallback for the path-specific (own-node) case. With no
matching update, `change` stays null and we emit the own node's CURRENT full value
(already filtered to reference.Path) — always the def, never a satellite. This
completes the fix the own-path stamp intended.

Pairs with the InstallReleaseRequestWatcher eager->commit high-water refinement and
the compile-completion backstop. Verified against the 80x CI repro loop (was
catching at iter 4-26; now green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rbuergi

rbuergi commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Closing: the flake this chased was root-caused and eliminated by #173 (single compile driver + complete terminal write). The watcher refinement here predates that rewrite and is unproven against it. Residuals (eager high-water advance, the standalone flake-repro harness, the timeout diagnostic) are tracked in #185 — the branch stays for cherry-picking.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant