Repository navigation
fix: the install no longer waits 30 s for a cover grant it documents as optional - #2849
Merged
Merged
Conversation
…as optional
`PackageInstaller` waited up to 30 s for a partition's cover grant while the
wait's own doc comment called that grant optional — "a partition whose node
type does not gate never writes it". On the shape the code itself called
*normal* the query can never emit, so every install burned the entire budget
and then reported success, leaving one Information line worded to cover both
outcomes. Measured in `MeshWeaver.PluginCatalog.Test`: 30.25 s / 30.29 s /
30.33 s / 30.24 s per install and 60.28 s for the test that installs twice —
181 s of a 336 s suite. `PackageInstaller.Install` is the production install
path, so a live mesh paid the same 30 s per non-gating package.
Core cannot ask a node type whether it gates (`Store/Plugin`, `PluginContent`
and `AddPluginGating` live in MeshWeaver.Plugins), which is why the grant is a
well-known PATH. It does not have to: `EnsureDeclaredAccess` is the ONE
access-establishment step of an install and already states, per manifest,
whether it published the partition or left it gated. `DeclaredAccessMarker` is
null exactly when it wrote nothing — the commercial branch — and that is the
only shape a gating pass owes a cover grant. `CoverGrantExpected` is that, and
the watch now runs only there.
- `GatingSettleTimeout` (30 s) → `GatingDetectorBudget` (5 s), renamed for what
it now is. Justified by measurement: when a grant IS owed it is observable in
44–55 ms (`GatingDetectorTest` measures the real write→observe latency on a
live mesh), and phase 1 has already paid the activation, so the only
outstanding work is one access-table write.
- The two outcomes stop looking identical: the expiry is a WARNING naming the
missing path and the consequence (the partition denies every viewer,
including on the Subscribe cover that would sell it); "nothing owed" is Debug.
Warning, not Error — the expiry proves the grant is not there yet, not that
it never will be, and `VerifyDeclaredAccess` owns the Error verdict for
access core itself promised to write.
- Still never fatal, and the query is unchanged: `path:{grant}` exact,
empty-on-absent, never a stream read (#2229) and never a children listing.
`GatingDetectorTest` pins all three outcomes and is mutation-proven: downgrading
the warning, disabling `CoverGrantExpected`, or restoring the 30 s budget each
turns it red.
Suite: 707/707 green, 5 m 36 s → 2 m 49 s.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a performance/diagnostic defect in the plugin install path where PackageInstaller always waited up to 30s for a “cover grant” it also documented as optional, by only running a bounded “deadlock detector” when a cover grant is actually owed (commercial packages) and making the “stalled” outcome explicitly visible in logs.
Changes:
- Introduces
CoverGrantExpectedand replaces the unconditional 30s wait with a 5s bounded detector (DetectGatingStall) that runs only for installs that deliberately leave the target partition gated. - Refines install warming to filter detector fan-out early and distinguishes outcomes (not expected / landed / stalled) with clearer logging.
- Adds a dedicated
GatingDetectorTestsuite plus new/updated documentation (including a What’s New entry and an architecture page) explaining the install readability model.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.PluginCatalog.Test/InstallGatingHandshakeTest.cs | Updates the handshake contract commentary to reflect “watching” only for deliberately gated partitions. |
| test/MeshWeaver.PluginCatalog.Test/GatingDetectorTest.cs | Adds live-mesh tests pinning the detector’s three outcomes and the budget expectations. |
| src/MeshWeaver.PluginCatalog/PackageInstaller.cs | Implements CoverGrantExpected, DetectGatingStall, renames/adjusts the budget, and wires the detector into root warming. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-31-installs-no-longer-wait-30-seconds-for-a-node-that-is-optional.md | Adds a What’s New entry describing the behavioral and performance change. |
| src/MeshWeaver.Documentation/Data/Architecture/Plugins.md | Documents why the cover grant is addressed by path and links to the new install-readability model. |
| src/MeshWeaver.Documentation/Data/Architecture/InstallReadability.md | New architecture page describing the two “readability doors” and the detector/budget rationale. |
| src/MeshWeaver.Documentation/Data/Architecture.md | Adds “Install Readability” to the architecture index under plugins/content delivery. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+622
to
+626
| .Catch<GatingOutcome, Exception>(_ => | ||
| { | ||
| logger?.LogInformation( | ||
| "[PackageInstaller] root {Root} has no cover grant after {Seconds}s. Either its " | ||
| + "node type does not gate (normal — a course copy or a plain partition), or the " | ||
| + "gating pass has not settled and the partition will DENY viewers until it " | ||
| + "does. The install itself is complete either way.", | ||
| root, GatingSettleTimeout.TotalSeconds); | ||
| return Observable.Return(Unit.Default); | ||
| logger?.LogWarning( | ||
| "[PackageInstaller] {Id} installed into {Root} GATED — it is commercial, so the " | ||
| + "installer published no access shape on purpose — but its cover grant {Grant} " |
Comment on lines
1533
to
1534
| var workspace = hub.GetWorkspace(); | ||
| var accessService = hub.ServiceProvider.GetService<AccessService>(); |
Comment on lines
+207
to
+212
| private IObservable<PackageInstaller.GatingOutcome> Detect( | ||
| PackageManifest manifest, string root, ILogger logger) => | ||
| Mesh.ServiceProvider.GetService<AccessService>() | ||
| .RunAsSystem(() => PackageInstaller.DetectGatingStall(Mesh, manifest, root, logger)) | ||
| .FirstAsync() | ||
| .Timeout(TimeSpan.FromSeconds(60)); |
Comment on lines
+272
to
+273
| public IReadOnlyList<(LogLevel Level, string Message)> Records => records; | ||
|
|
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
rbuergi
enabled auto-merge
August 30, 2026 22:31
Contributor
rbuergi
added this pull request to the merge queue
Aug 30, 2026
rbuergi
removed this pull request from the merge queue due to a manual request
Aug 30, 2026
rbuergi
added this pull request to the merge queue
Aug 31, 2026
rbuergi
added a commit
that referenced
this pull request
Aug 31, 2026
rbuergi
added a commit
that referenced
this pull request
Aug 31, 2026
…tifiers
CI reddened my own guard within the hour, and for a reason that had nothing to
do with what it guards:
Set: ["GatingDetectorBudget", "DeclaredAccessVerifyBudget", ...]
Not found: "GatingSettleTimeout"
#2849 renamed GatingSettleTimeout -> GatingDetectorBudget while this branch was
being written, and the self-test listed three bound names verbatim. So a guard
about SILENCE ON EXPIRY failed because a symbol moved.
That is the brittleness I flagged to myself while writing it and introduced
anyway. A guard naming a symbol inherits every rename of that symbol as a false
failure — and a guard that cries wolf gets deleted, taking the real check with
it.
The self-test's job is only to prove the scanner is not vacuous. It now asserts:
* a COUNT (>= 6 bounded waits found) — a regex matching nothing would make the
real assertion pass on an empty set;
* a SHAPE — each matched bound ends in Timeout / Budget / Bound, so the scanner
is provably matching budgets rather than incidental tokens, and this survives
any rename.
No hard-coded identifiers remain.
Also merges main, which is where the rename came from.
Re-proved after the change: restoring the silent swallow still fails the primary
assertion, naming file, line and bound. Documentation.Test 134/134;
MeshWeaver.PluginCatalog and MeshWeaver.Documentation.Test both
-c Release -warnaserror at 0 Warning(s) 0 Error(s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
PackageInstallerwaited up to 30 seconds for a partition's cover grant while the wait's own doc comment called that grant optional — "a partition whose node type does not gate never writes it" — and the catch's own message called the missing grant "normal — a course copy or a plain partition".Both statements were true at once, and together they made a deadlock that never went red. On the shape the code itself called normal the awaited node is never written, so the query can never emit: every install burned the entire budget and then proceeded green, leaving one
Informationline worded to cover both outcomes — so the healthy install and the wedged one printed the same sentence.Measured in
MeshWeaver.PluginCatalog.Testonorigin/main:SelfTypedRootInstallTest.SelfTypedRoot_ReinstallImmediately_WritesNothingStaleStampRootBindingTest.StaleStampSelfTypedRoot_RootServesItsTypesAreaStaleStampRootBindingTest.StaleStampSelfTypedRootShippingContent_RootServesItsTypesAreaSelfTypedRootInstallTest.RootTypedByAnInPackageNodeType_InstallsInstallReleaseOrderingTest.DeferredNodeTypeReleases_AreRequestedAfterTheRootRecycleOne install, one 30 s stall — 181 s of a 336 s suite.
PackageInstaller.Installis the production install path, so a live mesh paid the same 30 seconds for every package that does not gate.The discriminator core actually has
Core cannot ask a node type whether it gates —
Store/Plugin,PluginContentandAddPluginGatinglive inMeshWeaver.Pluginsand compile live on the mesh, which is exactly why the grant is addressed as a well-known PATH. It does not have to ask.EnsureDeclaredAccessis the ONE access-establishment step of an install, and it already states, per manifest, whether it published the partition or left it gated.DeclaredAccessMarkernames the node it wrote and isnullexactly when it wrote nothing — the commercial branch (any non-zero price, or a contact-sales address):{partition}/_Policy · PublicRead = truepublicSegmentspublicSegmentsPublicViewer grant — which IS the cover grantVerifyDeclaredAccessSubscribecover that would sell itPackageInstaller.CoverGrantExpectedis that and nothing more, restricted to the partition the manifest actually speaks for (a root the package wrote into but does not target is one core has made no access statement about, so it stays silent rather than inventing a verdict).What changed
CoverGrantExpectedgates the watch, so the case that can never succeed is not watched at all.GatingSettleTimeout(30 s) →GatingDetectorBudget(5 s), renamed for what it now is: a deadlock-detector budget, not a settle barrier. Justified by measurement, not taste — when a grant IS owed it is observable in 44–55 ms over repeated runs (GatingDetectorTest.Detector_sees_a_cover_grant_that_landsmeasures the real write→observe latency on a live mesh), and phase 1 has already paid the hub activation, so the only outstanding work is one access-table write.Subscribecover. "Nothing owed" isDebug. Warning and not Error deliberately: the expiry proves the grant is not there yet, not that it never will be, andVerifyDeclaredAccessalready owns the Error-level verdict for access core itself promised to write.path:{grant}exact — empty-on-absent, never aGetMeshNodeStreampoint read (Write lane degraded on memex-cloud: create chain dies on a NotFound stream, a finished Store/Provision storms compile-state, plus a permanent-silent import order bug and a 2/s identity-cache loop #2229 item A, where the routing NotFound openedMeshNodeStreamCache's storm-breaker and suppressed the very write the wait was waiting for) and never ascope:childrenlisting that can page past a grant that exists.The trade is named in the code: a gating pass slower than 5 s is now reported rather than waited out.
The detector is proven to fire
GatingDetectorTest(new, 6 cases) pins all three outcomes — nothing owed (0.27 s), the grant lands (44–55 ms), the grant never comes (bounded and loud, asserting the level, the exact missing path and the named consequence). It deliberately mirrors the budget rather than importing the production constant, so it cannot pass at 30 s the way the old code did.Mutation-proven — each of these turns it red:
LogWarning→LogInformation(the old, indistinguishable line)CoverGrantExpected→ alwaysfalse(detector never engages)Verification
dotnet test test/MeshWeaver.PluginCatalog.Test -c Release -warnaserror, each run alone on the same machine:Per test:
SelfTypedRoot_ReinstallImmediately_WritesNothingStaleStampSelfTypedRoot_RootServesItsTypesAreaStaleStampSelfTypedRootShippingContent_RootServesItsTypesAreaRootTypedByAnInPackageNodeType_InstallsDeferredNodeTypeReleases_AreRequestedAfterTheRootRecycleRealPluginsCheckout_ListsTheStore(control — unrelated)Also green:
test/MeshWeaver.Documentation.Test(129/129, including the guard that rejected an early.ToTask()in the new test — it is fully reactive now) andtest/Memex.Portal.Shared.Test(490/490; one run showed the known 31 s contention flake inPluginBundlePlanTest.PromotingTheInstance_WidensTheNextRequest, which passes in isolation in 1 s and passed on two subsequent full runs of that suite).Work products committed
Per the conserve-work-products rule:
Doc/Architecture/InstallReadability(the two doors an install can open, the detector and its budget, the "never block on a node you documented as optional" rule, and the measured arithmetic), linked fromDoc/ArchitectureandDoc/Architecture/Plugins, plus a What's New entry.🤖 Generated with Claude Code