CI disposal audit — 2026-09-08

CI evidence and its limits

The audit read the latest 30 failed Catalog runs and 40 earlier failed runs through GitHub REST, inspected failed-job inventories on nine representative runs, and downloaded three portal-host job logs. The working tree was 962f8702, with CI platform source e1039813e4c080f46d6e54b7a84b09c92326856e.

Evidence Finding Attribution
Run 34210183539, shard 1 and run 34222981863, shard 1 FutuRe test host exits 139 at about 1m46s. Native crashes; these logs do not prove a disposal cause. The related #1527 investigation records zero-MethodTable GC crashes and a separate managed query leak.
Run 34235028593, shard 2 First compile assertion fails in 424ms with NullReferenceException; the process later fails many compile cases and times out. The compiler canary reports failures even with fresh image-backed references, while flat source still emits. This is the existing Roslyn/CLR investigation, not evidence that every subsequent test independently leaked.
#1390 AI Orleans host reports a disposed Autofac scope while all 51 tests pass. The managed lifetime family is real; the tracked AI fix had already landed before this audit.
#1513 Teardown NACK assertion times out at six seconds. Current main already replaces the timing inference with observation of owner shutdown and verdict consumption.

Several sampled reds are policy, validation or bundle-build failures, not intermittent tests. The query-connection leak documented in #1527 is in core's MeshNodeStreamCache, and the pinned platform used for this audit already contains its connection ownership fix. Do not copy that fix into this repository or describe a later green FutuRe run as proof that the native crash is cured.

Instance-sync cleanup gaps found in the source audit

InstanceSyncWorker.DisposeClientLocked discarded the ValueTask returned by the remote client's DisposeAsync. Its comment said the implementation never faults. That says nothing about when it finishes: the MCP client can still be awaiting connection/client teardown after the worker returns. Neither the hub completion signal nor the I/O pool drain counts a discarded cleanup task.

The worker now captures the mesh-scoped AsyncDisposeQueue at construction and queues each detached client's cleanup. Mesh teardown already drains that queue before releasing the service scope. The cached reference is cleared before enqueueing so replacement and repeated disposal do not dispose the same client twice. The client-cache gate also protects the terminal flag: a callback that reaches GetClient after disposal cannot allocate a new transport nobody will release.

InstanceSyncCoordinator retained the change-feed subscription but discarded the startup query's subscription. Stopping before its initial result left discovery alive for up to its 30-second timeout, with a callback capable of starting workers after the stop. Each start now owns both subscriptions in a CompositeDisposable, released before its workers. A fresh bag per start preserves the existing same-instance stop/start contract. A locally captured bag also disposes a subscription whose registration returns after that particular run has stopped.

PR review identified the remaining dispatched-callback race: unsubscription cannot retract an already captured feed callback. A lifecycle gate now serializes worker creation with the stop snapshot, and each callback must still belong to the current start. Stop invalidates that start and detaches its workers under the gate, then disposes subscriptions and workers outside it so unsubscription can safely join a callback. A callback from a stopped run cannot populate even a later restart. Two fenced delivery cases cover both stopped and restarted coordinators.

These are concrete lifecycle defects, but the downloaded CI logs do not establish that either caused the FutuRe native crashes or the Roslyn failure above. The change does not adjust timeouts, disable parallelism, retry CI until green or suppress exceptions.

Validation

InstanceSyncDisposalTest drives the actual client cache and disposal path with a remote client whose cleanup completion is explicitly held open. The drain must stay pending until the test releases cleanup. Separate cases cover client replacement, repeated disposal, refusal to reopen a disposed worker, and cancellation/restart of boot discovery. These assertions test ordering without relying on a lucky scheduler interleaving.

The complete instance-sync suite must also run under DOTNET_PROCESSOR_COUNT=4; its persisted manifest restart case catches a terminal subscription bag accidentally reused after StopAsync.

Measured on .NET 10.0.11, against the platform pin above:

The regression tests establish the defects deterministically. Three loaded passes are interaction coverage, not a statistically established cure for a low-frequency native crash. No matching before/after CI failure rate has been measured for the separate FutuRe/Roslyn families.

Reconnecting…
The server was updated. Reloading the page to pick up the latest version.