Repository navigation
A package root is disposed while its own install is in flight (CD 7950): the creates' write-through streams lose their owner, no reply is ever sent, and the install runs out its bound — neither #3501 nor #3505 covers it #3510
Description
Measured: CD 7950 (f55b9f4ab, carries #3480 and #3475, neither #3501 nor #3505) failed its seal on a THIRD mechanism
[FAIL] Hosting — install: TimeoutException after the install's own 10-minute bound (── Hosting: installing 145 file(s)… at 23:36:36Z; the next package starts at 23:46:36Z). Every discriminator that names the two mechanisms fixed tonight reads zero over the whole bake log: VERDICT_TIMEOUT=0 (#3499/#3505), resubscribe failed=0 (#3498/#3501), LATE_NACK_REENQUEUE=0, OWNER_NACK_REENQUEUE=0, PATCH_FLUSH_FAULTED_SYNC=0. The control 7938 (sealed) reads zero on all of them too — so this run's failure is something else, and the trail says what.
The trail (request-fate ledger, printed by [STALE-CALLBACK])
Four creates for Hosting's own nodes, all CreateOrUpdateNodeRequest → portal/nodeops-1SDY…, pending 30–60 s:
Hosting/Backup: … ROUTED onTarget=True state=Submitted@portal/nodeops(+5518ms) → HANDLER_ENTER → UPSERT_READ existing → update
→ UPSERT_WRITE_THROUGH_STREAM path=Hosting/Backup/_Activity/compile-state → HANDLER_EXIT state=Processed(+5519ms)
⇒ a handler was entered and no reply, completion or fault has been recorded since
(same for Hosting/BackupStore, Hosting/ConfigAudit, Hosting/Deployment)
and, in between, the owner of every one of those paths stops answering:
23:37:44 [UpdateQueue] ADVANCE_WITHOUT_HANDOFF path=Hosting/Admin/_Activity/compile-state seq=338 bound=5000ms — the owner never acknowledged this write
23:37:44 [UpdateQueue] ADVANCE_WITHOUT_HANDOFF path=Hosting/ConfigAudit/_Activity/compile-state seq=339
23:37:44 [UpdateQueue] ADVANCE_WITHOUT_HANDOFF path=Hosting/BackupStore/_Activity/compile-state seq=340
23:37:51 [QUIESCE-TIMEOUT] Hosting: 1 callback(s) still pending after 2s — forcibly cancelling. Pending: CreateOrUpdateNodeRequest@portal/nodeops (… UPSERT_READ absent → create → HANDLER_EXIT ⇒ no reply recorded)
23:37:51 [PluginGating] reconcile failed for Hosting MeshWeaver.Messaging.HubDisposedBeforeResponseException: Hub Hosting was disposed before the response arrived
23:38:01 [UpdateQueue] ADVANCE_WITHOUT_HANDOFF path=Hosting/Backup/_Activity/compile-state seq=352 … Hosting/_Access/Public_Access seq=353 … Hosting/Deployment/_Activity/compile-state seq=355 … Hosting/LogEntry/_Activity/compile-state seq=366 … Hosting/BackupStore seq=369
23:38:25–23:38:54 [STALE-CALLBACK] Hosting/Backup, Hosting/BackupStore, Hosting/ConfigAudit, Hosting/Deployment — the four creates, 30…59 s and climbing
Reading
The Hosting root hub was disposed at 23:37:51Z while Hosting's own 145-file install was in flight. Its per-node children — the owners of Hosting/*/_Activity/compile-state, Hosting/_Access/Public_Access, Hosting/BackupStore — went with it. The nodeops handler had already written through those node streams and exited, owing its reply to the write's ack; the ack never came (ADVANCE_WITHOUT_HANDOFF, owner never acknowledged), so the reply was never sent, the installer's Observe never fired, and the install ran out its bound. This is the sequence #3499 was FILED as — "a package's ROOT hub is disposed mid-install" — and it is exactly what #3499's root-cause work set aside: the [QUIESCE-TIMEOUT] count peaking in the sealed run showed that a root recycling under a reconcile is usually benign, which is true, but the discriminator is not the count — it is whether the recycled root is the package currently installing. Here it is. #3505 (classification of a committed patch's durability fault) cannot reach it: these are creates through nodeops, not UpdateRemote patches, and no verdict was ever produced to classify. #3506 (a hub past [QUIESCE-OK] still taking intake) is not it either: RECEIVED runLevel=Started@Hosting — the root was healthy when it took the request and was torn down under it.
Who disposed the root is not in the log at this level — no [QUIESCE-START] Hosting, no dispose source line. The leading candidate is the NodeType rebind recycle (the package's own NodeTypes compile during its install; a rebind posts DisposeRequest to the root), i.e. #3499's hypothesis (2), which was never measured because (1) was found first. The next occurrence needs the dispose source at Information on the bake host, or [QUIESCE-START] naming the reason.
A second, distinct anomaly in the same log, recorded so it is not lost: HomeAssistant's create trail ends RECEIVED@portal/nodeops → ENQUEUED@portal/nodeops → ROUTED onTarget=False state=Forwarded@mesh(+2ms) — the nodeops hub took the delivery and forwarded it AWAY as not-on-target, 33–48 s pending, then resolved. A routing bounce, not the disposal shape; HomeAssistant installed.
Expected
An install's writes must not lose their owner to a recycle of the very root being installed: either the recycle is deferred while an install holds the root (the install is the writer that should own the root's lifetime), or the recycle answers every in-flight write under it with a NACK the nodeops handler turns into the installer's reply, so the install fails in milliseconds with a name instead of running out a 10-minute bound. And [QUIESCE-START] on a root should name who asked.
Consequence for #3355
Live in 7950 with #3480 in, absent by count in 7938; base rate on this exact shape tonight: 1 run (7937 and #1422 were #3499's, 7941 was #3498's). By the transitive rule it joins the blocker set; the candidate 7960 (155b4fa17) can meet it. Found reading 7950's bake log with seal-read.sh.
Activity
rbuergi commentedon Sep 7, 2026
Attribution: the disposer is
PackageInstaller.SettleRetypedRoot, and the recycle is not the defectMeasured on core CD 7950 (
f55b9f4ab, #3480 in, neither #3498 nor #3499), bake job101577531852. Every elimination
below is made from code, not from a line's absence.
It is not #3499, #3498 or #3506
VERDICT_TIMEOUTPATCH_FLUSH_FAULTED_SYNCLATE_NACK_REENQUEUE/OWNER_NACK_REENQUEUEJsonSynchronizationStream … resubscribe failedRECEIVED runLevel=…@Hostingat intakeStarted#3505classifies a verdict; here none is produced at all.#3506's intake window is excluded bythe trail: the root was healthy when it accepted the request.
Who disposed
HostingPackageInstaller.SettleRetypedRoot— the installer's own, deliberate, ordered recycle of theroot of the package it is installing. Code-proven:
PackageInstaller.cs:2945—retypedRoot = placeholderRoot is not null ? root?.Path : null, andplaceholderRoot(:2500) is non-null iff!rootTypeIsStatic, i.e. iffFindStaticNode(root.NodeType)is null.Store/Plugin, and the code states it is dynamic in two places:PackageInstaller.cs:92— "partition ROOTS whose node types are dynamic (e.g. Store/Plugin)" —and
PartitionTeardownCoverageGate.cs:89— "declared in mesh content — Store/Plugin — that nosrc/-side registration covers".
SettleRetypedRootfires on every plugin package install.:2994places it immediatelyafter adoption + the first release wave, and the comment beside it names this exact hazard:
"pointed a whole package's compiles at a hub this very method was about to dispose (PackageInstaller launches every NodeType compile fire-and-forget, then deliberately disposes the root hub those compiles are reading #1732)."
The timing fingerprint — 4/4 packages, disposal always inside the package's own install, always after its own adoption
installing N file(s)…Never before the adoption; once per package. No other candidate predicts that shape.
🚨 The recycle SUCCEEDED, and is not the defect
[PackageInstaller] root … never answered after its recyclewarnings — the root came backinside
RootReadyTimeout(20 s) every time.reconcile failed for <pkg>: Hub <pkg> was disposed before the response arrived) appears 7–12× per bake in every run examined, including the sealed control #7938(12 occurrences, sealed clean).
So "the root of the package being installed is disposed under its own install" is by design and
survivable. What is not survivable is the work in flight across it never being answered.
Eliminations, each from code
NodeTypeEnrichmentHelpers)AutoRecycleOnStaleBuildreadsModules:AutoRecycleOnStaleBuild; the tester never sets it ⇒false⇒ the watcher offers, never disposesOkHubRecycleExtensions.RecycleNodeRecycleLayoutArea, a circuit-only GUI confirm flow; no circuit in a bakePluginGatingDisposeRequestoutside tests andMeshPlugin.cs's MCP tool — the disposer is in coreNodeTypeRebindWatcherThe actual defect — a totality hole on the UPSERT lane
MeshExtensions.cs:~4740/~4930subscribe to the innerCreateNode/UpdateNodewithonNext/onErroronly. When the owning root is disposed under the request the inner observableneither emits nor errors, so no terminal arm runs, no
PostFailis posted and nothing is logged.The trail ends exactly there:
Confirmed by the denominator: zero
[CreateOrUpdate]lines of any kind in the entire 7950 log —silence, not a fault. Unlike the patch lane there is no
WriteVerdictBoundequivalent to convertthat silence into a verdict, which is exactly why every counter above reads zero. Same class as
#3033 / #3194, one lane over.
Attribution gap this measurement hit, and what closes it
SettleRetypedRootlogs nothing on the recycle path — only the decline(
"not recycling root {Root}", Information). So the tester log-level change in #3505 names thedecline and
[QUIESCE-START], but never the positive recycle. OneLogInformationat theDisposeRequestpost closes it, and is part of the fix.rbuergi commentedon Sep 7, 2026
Fourth occurrence — CD 7967 (
4c89dc6b6, the first run carrying #3512) — and the transcript now names the recycle itselfTally on runs that started with #3480 in: 7950 failed, 7959 failed, 7962 hit it and sealed, 7967 failed — four occurrences, three failed seals. Same package every time: Hosting, 145 files. All counters zero again (VERDICT_TIMEOUT, both re-enqueue arms, PATCH_FLUSH_FAULTED_SYNC; resubscribe-failed 3, elsewhere),
ADVANCE_WITHOUT_HANDOFF path=Hosting/…×12, install bound at 10 min.With #3512's line, the attribution that took a code read on 7950 is one grep:
Two things the labelled trail adds:
RegisterOwnerDisposingNack→ re-enqueue, measured green) and false forUPSERT_READ absent → create, where the pendingCreateNodeRequestis what[QUIESCE-TIMEOUT]cancels 10.6 s later and nothing answers. The sentence is the specification; the create leg is the gap.Hosting/Admin/_Activity/compile-stateat 03:44:13.8, recycle at 03:44:19.1). So at least one write under Hosting had already lost its owner beforeSettleRetypedRootfired — a per-type rebind or release-wave teardown of that child, not the root recycle. Whoever drives the reproduction should park the seam for BOTH: the root recycle and a child owner going away mid-create.Base rate now: 3 of 4 started runs failed on this; the one that passed (7962) had the same three un-terminated compile-state writes (its
VERDICT_TIMEOUTlines) and the installer simply was not waiting on them. Whether an install fails is whether the abandoned create is one the installer awaits.rbuergi commentedon Sep 7, 2026
The seam to park is the CHILD owner, not the root — which is why two root-park attempts failed
Adding the piece that connects the 7967 trail to the reproduction attempts, because together they name the seam and separately neither does.
What was tried, and how it failed
While building this fix I drove the teardown twice and could not put a create behind a parked hub:
That read as "the harness cannot reach the seam". It was more specific than that.
What 7967's trail shows
🚨 The first
ADVANCE_WITHOUT_HANDOFFis 5.2 seconds BEFORE the root recycle. A child owner went away mid-create whileSettleRetypedRoothad not yet fired.That is the explanation for the failed fences. The create routes to the child (
Hosting/Admin/_Activity/compile-state), not to the root — so parking the root's stream parks something the create never traverses. The root recycle is a later, louder event that happens to be in the same window; it is not the seam.What that means for the fix
The reproduction must park a per-type teardown UNDER the root, on the path the inner
CreateNodeactually traverses — not the partition root's primary stream. That is the concrete form of the earlier note that this needs "a park on the seam the innerCreateNodetraverses, or extracting the inner-write subscription into a pure composition the wayArmPatchAckWatcherwas for the patch lane." The second remains the better shape because it makes the totality drivable without a mesh at all.🚨 The recycle message's own sentence is the contract, and the create leg is where it is false:
On the update leg that is true — measured:
RegisterOwnerDisposingNackmintsOwnerDisposing, the writer re-enqueues, the caller getssuccess=True. On the create leg it is false: the inner observable neither emits nor errors, no terminal arm runs, noPostFail, no diagnostic. Worth quoting verbatim as the fix's test name — a contract stated in a log line and violated one lane over is exactly what a regression test should pin.Scope, unchanged: the update leg is already total. Only the create leg (
UPSERT_READ absent → create) is uncovered.On the log line
It was added in #3512 for precisely this and named the recycle on its first run — 03:44:19.082, with
adopted 15 prebuilt assembly(ies)immediately before it. Attributing the same event on 7950 previously required a full code read ofPackageInstaller. That is the whole value of the line and it is now demonstrated rather than argued.Occurrence count
Four occurrences, three failed seals, of four started runs since #3480 — Hosting every time. The one that passed (7962) hit the shape and survived; what separates them is presumably whether the un-terminated write is one the installer waits on.
Hostingis the largest plugin package (145 files) with a dynamic root, which fits a window that scales with the install's own write count.rbuergi commentedon Sep 7, 2026
Fifth occurrence: CD 7968 (
e64689e7f, the workflow_run twin of the commit 7969 sealed) failed its seal at 05:07:27Z on the same shape —[FAIL] Hosting — install: TimeoutException, all counters zero (VERDICT_TIMEOUT, both re-enqueue arms, PATCH_FLUSH_FAULTED_SYNC), resubscribe-failed 2 elsewhere. Tally over the eight runs that STARTED since #3480: sealed 7955, 7962, 7964, 7969; failed 7950, 7959, 7967, 7968 — every failure is this shape, Hosting every time, and 7962 carried the same un-terminated compile-state writes and sealed because the installer was not waiting on them. Roughly one started run in two loses its seal to this.rbuergi commentedon Sep 7, 2026
Sixth occurrence: CD 7976 (
19b077868, workflow_run) failed its seal at 06:09:32Z on the same shape (Hosting install bound, all counters zero, resubscribe-failed 1 elsewhere). Tally over the ten runs that started since #3480: sealed 7955, 7962, 7964, 7969, 7974; failed 7950, 7959, 7967, 7968, 7976 — every failure this shape. One started run in two.rbuergi commentedon Sep 7, 2026
🚨 The create-leg attribution is REFUTED by the sixth occurrence — the park has no pending callback at all
Measured on CD 7976 (
19b077868, bake job 101634597130), by reading the whole bake log rather than the counters.The two facts that decide it
The one stale callback in the Hosting window took the UPDATE leg, and it resolved.
Not
UPSERT_READ absent → create. And it was reported once: the reporter re-reports every 5 s, so it was answered between 30 s and 35 s.During the eight-minute park that actually ran out the bound, NOTHING was pending.
[STALE-CALLBACK]fired 4× for HomeAssistant minutes earlier in the same run and 0× across the park. Every[STALE-CALLBACK]in the entire log: HomeAssistant ×4, WebSearch ×2, Training ×1, Hosting/InstanceRequest ×1 — none of them inside the park.An unanswered
CreateOrUpdateNodeRequestis a pending callback. The install was therefore not waiting on a node op; it was parked on an observable that never terminated. The create-leg totality gap is real — it just is not what loses these seals.Where it does park
The control is in the same log.
Edu— same shape, same run — printed[PackageInstaller] warmed installed root Edu2.5 s after its own deferred release wave.warmed installed root Hostingnever printed at all, andWarmInstalledRootsruns immediately afterRequestReleases(deferredWave)— so the park is inside the wave.Why the wave and not something else in that segment, eliminated from code:
AffectedNodeTypescarriesTypeEnumerationBudget+ aCatch;SeedPrebuiltAssembliescarriesSeedBound; the trigger write carriesBaseStateWaitBound(30 s) then verdict windows ofLateResponseWatchBound + VerdictBoundGrace(31 s) acrossMaxConflictRetries.nodeTypePaths.Select(ObserveNodeTypeRelease).Merge().ToList()carries none — whichMeshNodeStreamHandle.BaseStateSource's own remarks named in advance: "no per-leg bound and no outer bound — so ONE non-terminating leg parks the entire package install, silently, until the gate's own 600 sInstallTimeoutreportsinstall: TimeoutExceptionagainst a package that installed fine 8 minutes earlier."The defect, and the fix
ObserveNodeTypeReleasepromises exactly one emission, always (its remarks, and its closingDefaultIfEmpty(false)). It delivered that for three of Rx's four outcomes —DefaultIfEmptycovers "completed empty", twoCatches cover "faulted". Nothing covered "never terminated at all", which neither operator can observe.#3550 adds
BoundReleaseLeg:Take(1)first (so the deadline is TOTAL, not Rx's inter-emission one — the trapBaseStateSourcewas rewritten to remove), thenReleaseRequestBound, whose elapse answersfalse, logs at Warning and names the NodeType. 180 s: strictly above the ≈124 s a legitimate write can compose, far below the installer's 600 s. Nothing retried, nothing swallowed, no existing bound moved.🚨 It does not claim to explain why a leg stops answering — that stays open, and the next occurrence will now name the type in one line instead of costing a full log read. Pinned by
ReleaseWaveLegIsTotalTest(pure composition,TestScheduler, no mesh) — including the counterfactual: the unbounded wave over the same input still has not completed at 900 s.Committed to
Doc/Architecture/BakeSealNodeOpsSaturationand corrected inDoc/Architecture/WriteVerdictTotality.rbuergi commentedon Sep 7, 2026
Landed — #3550, merge commit
eb2b8d76a(08:11:46Z)Deliberately leaving this issue open, because what merged is not what this issue names.
What is fixed: the install no longer parks.
ObserveNodeTypeReleasenow keeps the contract italready stated about itself — exactly one emission, always — for all four of Rx's outcomes, so one
non-terminating leg can no longer hold
Merge().ToList()and, with it, the whole package installuntil the gate's 600 s bound. A leg that does not answer within
ReleaseRequestBound(180 s, orderedabove the ≈124 s a legitimate trigger write can compose and far below the installer's 600 s) is
answered
false, logged at Warning, and names the NodeType.What is NOT fixed, and is what should keep this issue open: why a release leg stops answering.
The eight-minute park in 7976 had no pending callback and produced no log line, so there was nothing
in the transcript to attribute it with. There is now.
On the next occurrence, this is the whole triage:
One line, naming the NodeType path. That turns the residual from "read the whole bake log and
eliminate from code" into a single grep — which is the point of landing this half first.
Also corrected:
Doc/Architecture/WriteVerdictTotality's #3510 section. The upsert lane's CREATEleg gap is real and worth closing on its own merits, but it is not what fails these seals; 7976's
only stale callback took the UPDATE leg and resolved inside 35 s. Worth a separate issue rather than
staying filed under this one.
rbuergi commentedon Sep 7, 2026
The create leg is now costing the fleet's critical path — a live sighting on Plugins #1463
This issue's remaining half (the upsert's create leg has no disposal-NACK) has just wedged the
MeshWeaver.Plugins pin move, which is currently blocking six PRs and five issues.
The sighting (measured by meshweaver-64 on Plugins run
34115469651, job101732362683, gateshard 1, tester log
gate-log-8050550ab…-shard-1):Hostingnever reached its── Hosting: installedmarker. Per-package pace is otherwise unchangedagainst the green run (median 11.0 → 11.5 s; on 7938
Hostingtook 108 s), so this is not asystematic slowdown — it is one package hanging.
It matches this issue's own recorded fingerprint exactly.
7dd16fad0's measurement lists thedisposal timing per package —
HomeAssistant +9.6s,Hosting +7.9s,Cornerstone +14.9s,RemoteControl +1.7s— always inside the package's own install and always after its own bundle'sadoption.
PackageInstaller.SettleRetypedRootrecycles the root it just retyped, and it fires forevery plugin package install (
retypedRootnon-null iff!rootTypeIsStatic;Store/Pluginisdynamic).
And core already says which leg.
test/MeshWeaver.Graph.Test/UpsertAnswersWhenTheOwnerGoesAwayTest.cs::31HANDLER_ENTER → UPSERT_READ absent → create → HANDLER_EXIT state=Processed ⇒ (nothing):51"7950's trail isUPSERT_READ absent → create— the CREATE leg, which has no disposal-NACK":77"the point of pinning it while A package root is disposed while its own install is in flight (CD 7950): the creates' write-through streams lose their owner, no reply is ever sent, and the install runs out its bound — neither #3501 nor #3505 covers it #3510's create leg is still open"That commit added 15 lines to
PackageInstaller.cs, a doc, and the 177-line test pinning thecovered (update) leg. The create leg is untouched.
Shape.
DispatchInnerCreate'shub.Observe(inner, …).Subscribe(…)has two arms — onNext andonError. When the owner is disposed the response never arrives at all, so no arm ever runs. This
is the same class as #3550: a leg that can never terminate, not one that completes empty.
DefaultIfEmptyis the wrong repair, for the reason the read leg's own comment already gives — anempty read there is turned into a create, which would be a silent write on missing information.
Ruled out, so nobody repeats the bisect: #3509, #3515, #3491, #3505, #3480. None of them is the
cause; the defect predates all five and 7991 merely made it legible.
Recorded 2026-09-07 during a fleet-wide sweep, from meshweaver-64's measurement plus a read of
core at the two pins (
aa4075832green,67bd3d064red).fix(#3510): the upsert's CREATE leg answers instead of hanging forever
rbuergi commentedon Sep 8, 2026
Seventh occurrence — CD 8078 (
4fedf4549, carries #3512, #3550 AND #3603) — a FOURTH arm: the UPDATE leg's post-commit flush parks under the root recycleMeasured on run 34190841613, bake job 101955088724, control = the sealed 8076 (34186868747, job 101941978797, same core commit, same identity
s190dc0ca…). Full measurement and the code trace are on #3680 (the alert's auto-filed issue); the short form:idempotence: Chess — re-install of Chess/_Policy … TimeoutException— an UPDATE (PatchDataRequest), not a create, so fix(#3510): the upsert's CREATE leg answers instead of hanging forever #3603's create-leg NACK cannot reach it, and the trail shows nonack=OwnerDisposing: the per-node OWNER was never disposed. It stamped the patch (PATCH_MERGE_STAMPED v=4) and echoed it (PATCH_ECHO_SEEN+5 ms) and then recorded nothing — noPATCH_FLUSH_SUBSCRIBED, no bound armed, no ack, noVERDICT_EXPIRED, no "reached no route". The verdict was never minted.[PackageInstaller] recycling root <X> now that its in-package NodeType has a loadable build(Hosting 06:11:45 → sixHosting/*writes; Chess 06:24:07 → sevenChess/*writes, the[QUIESCE-TIMEOUT] Chess … forcibly cancelling+HubDisposedBeforeResponseExceptionin between). The control's single loss has the identical shape (recycling root Hosting05:00:41 →FAILED Hosting/InstanceRequest/_Activity/compile-state05:01:15).8b974323d): the flush isStoragePostCommitFlush.Build → WriteAndPublishUpdated → RoutingProxyAdapter.Write(.Take(1), no bound) — a routed write to the PARTITION hub whose prologue (_router.AddressFor,PersistenceService.TryWriteFrom's sequentialDefer) runs on the owner's echo thread BEFORE the 10 s flush bound is armed (DataExtensions.cs~1560–1590, the "Bake wedge: portal/nodeops stops draining mid-bake — one shared node-CRUD hub seen from eight places (queue latency bimodal: <=3.2s vs 33-49s) #2543 arm 1" window the code names). With the root mid-recycle the write has no live target, the prologue parks, no bound exists to fault it, and the caller times out at 31 s.Taking this arm (meshweaver-64): deterministic repro first (a patch whose flush meets a recycling root), then the recycle drains or the flush faults — never the bounds. Cross-linked from #3680.
rbuergi commentedon Sep 8, 2026
The disposer is named: it IS the installer's own retyped-root recycle
This issue's central open question — "Who disposed the root is not in the log at this level … The leading candidate is the NodeType rebind recycle … The next occurrence needs the dispose source at Information on the bake host, or
[QUIESCE-START]naming the reason" — is now answered by measurement. #3603's "THE RECYCLE NAMES ITSELF" line landed 2026-09-07 14:16Z, and the next occurrence read it.CD run 34190841613, 2026-09-08, package
Chess(4fedf454, which carries #3603):So it is
PackageInstaller.SettleRetypedRoot— the installer's own retyped-root recycle — not the NodeType rebind watcher. That also falsifies #3499's stated reading, which set this candidate aside: "The Chess root is aStore/Plugininstance (PluginContent), so this is not the installer's retyped-root recycle." It is.Two further details the trace settles:
The recycle's own claim is false as written. The line says "Work in flight beneath this root is answered by the teardown, not abandoned." The pending
CreateOrUpdateNodeRequestwas neither answered nor NACKed — it was forcibly cancelled at the 2 s quiesce bound and surfaced to its issuer asHubDisposedBeforeResponseException. That is precisely this issue's Expected, still unmet.This occurrence is the intake-during-quiesce shape, which A package root is disposed while its own install is in flight (CD 7950): the creates' write-through streams lose their owner, no reply is ever sent, and the install runs out its bound — neither #3501 nor #3505 covers it #3510's
Hostingcase explicitly was not. The request-fate ledger readsRECEIVED runLevel=**Quiescing**@Chess(the Hosting trace readrunLevel=Started@Hosting, which is why A hub past [QUIESCE-OK] still accepts new correlated requests — the intake gate opens one phase too late #3506 was ruled out there). Two of the pending callbacks came from the root's ownPluginGatingreconcile, one in flight at dispose entry and one issued after quiescing began — so the root kept taking correlated work from its own background pipeline while being torn down under it.There were two recycles of
Chessper bake in the run that passed 65 minutes earlier (34186868747, 05:13:01Z and 05:13:50Z), each killing a reconcile pass — and the same double-recycle is visible onVideoat 06:25:06.9 and 06:25:08.6, the second on an install that wrote 0 nodes (0 written, 21 unchanged). A no-op install still recycles the package root.Scope note
This recycle race is not what made that run's seal go red, so fixing it would not have saved it. The red came from a second, independent defect — core's pre-#902 access heal fighting
PluginGate.SeedGatingover_Policyand the child denies until the heal's own write starved for 20 s — fixed in #3681, with the analysis inDoc/Architecture/PartitionAccessOwnership. The two compound (the recycle kills the reconcile pass, leaving more un-converged residue for the heal to find) but neither causes the other.Recorded so the attribution is not lost. No bound was widened.
rbuergi commentedon Sep 9, 2026
#3800 merged — the pointless half is gone; leaving this open for the half that has a job
What landed:
SettleRetypedRootnow declines when the install wrote nothing, on the same condition as the twoRequestReleaseswaves it sits between. That inconsistency was the bug — the waves know a no-op install has nothing to recompile, and the teardown between them did not.Directly removes the two shapes measured on CD 34190841613:
Video(0 written, 21 unchanged, recycled anyway) and the second ofChess's two recycles per bake.And the positive log line stopped over-promising. It ended "Work in flight beneath this root is answered by the teardown, not abandoned." — measurably false in that same trace: the pending
CreateOrUpdateNodeRequestwas force-cancelled at the 2 s quiesce bound and reached its issuer asHubDisposedBeforeResponseException. It now says that.What keeps this open
An install that did write still recycles a root whose background pipeline is issuing correlated requests. The thread's own reading names the deeper half —
RECEIVED runLevel=Quiescing@Chess, i.e. the root kept taking correlated work from its own pipeline while being torn down under it. That is a hub accepting work past quiesce; its own defect, its own family, and not something to merge into a recycle-gating change. Widening the 2 s window would be the band-aid.Pinned by
RetypedRootRecycleNeedsAJobTest— 5 pure cases including the control (AnInstallThatWroteSomething_StillRecycles), because a predicate that declined every recycle would satisfy the regression case while silently re-breaking the binding the recycle exists to fix (#1732).rbuergi commentedon Sep 9, 2026
One of the three Expectations, in flight: #3821
#3821 does that. It matters more than its size because it is the Expectation that gates the
others: this issue could not identify the disposer at all, so "the leading candidate is the NodeType
rebind recycle … #3499's hypothesis (2), which was never measured" stayed unmeasured.
🚨 And the obvious source could not have supplied it.
Dispose()posts itsShutdownRequestto itself, so that message's
Senderis always the dying hub — the field that looks like itanswers who asked is the one field that cannot. Anyone reaching for it would have got a
confidently wrong answer. The discriminating fact is one frame earlier, in
HandleDispose: whethera
DisposeRequestarrived over the bus at all, and from where.Three readings, and each rules something out:
requested by portal/installer (routed DisposeRequest)PackageInstallerposting to a package root is exactly this issue's shaperequested by itself — a self-posted DisposeRequest (…), i.e. a rebind or self-heal recyclerequested by a direct Dispose() (no routed DisposeRequest)usingThe self-posted case is spelled out rather than printed as an address on purpose:
NodeTypeRebindWatcherand
WithOverlaySelfHealpost to themselves, so a bare sender rendersHosting: requested by Hosting— true, useless, and easy to misread as a routing oddity. That is this issue's leadinghypothesis, so it is the one reading that must not be ambiguous.
What is still open here, and stays open
so the install fails in milliseconds with a name instead of running out a ten-minute bound.
Neither is touched. #3800 (merged today) removed a trigger — the retyped-root recycle no longer
runs when it has no job — and
3893dc486(#3683) armed the patch-flush bound before the flush isbuilt so a root recycle cannot strand the verdict. Both reduce the occurrence; neither is the
contract this issue asks for. This issue should not be closed on #3821.
🚨 One caution for whoever picks up the rest, learned on MeshWeaver.Reinsurance#178 today: I expected
3893dc486to be the transition there and it was not — three gate-shard runs went green on thepre-fix binary. Occurrence counts moving is not evidence a specific commit moved them, especially
where the runtime is a floating tag.
rbuergi commentedon Sep 9, 2026
#3832 is merged at
7f37c487bf63bffaa9b61d0c60e08608d63fcc0b, after successful PR and merge-queue CI. It fixes the separately measured own-outgoing intake hole: while Quiescing, a hub no longer treats its own new awaited request as third-party transit merely because its destination is elsewhere.The deterministic regression failed before the fix because the destination answered normally. Coverage after the fix verifies immediate ShuttingDown refusal attributed to the origin, inbound refusal, existing reply drain, and third-party forwarding; the full messaging suite passed 405 tests without skips.
Leaving #3510 open. This closes admission of new owned requests during quiesce, not the remaining contract for creates already in flight across an install's root recycle. #3833 separately corrects the proven false NOT applied claim on an inner-create timeout and is still in the queue. No live rollout verification is claimed here.
rbuergi commentedon Sep 10, 2026
Reopened — the 2026-09-10 sweep was not a triage decision
This issue was closed
not_plannedat 2026-09-10T14:31:18Z by a bulk sweep that PATCHed every open issue in all 13Systemorphrepositories — 311 issues in twelve minutes, no comment on any of them — after a session was asked to "close all issues in all repos". Nothing on this thread was read, and no judgement was made about this issue.All 82 issues the sweep closed in the MeshWeaver fleet were reviewed afterwards. This one is unresolved: Several arms landed; the Expectation this thread says gates the others has not.
Sixteen of the 82 were left closed — their fix had actually landed, or the author had withdrawn them; those should have read
completed, notnot planned.docs(#3510): record the rebind elimination and where the reason is read
fix(#3510): the lease names the root the install WRITES under, and it…
rbuergi commentedon Sep 11, 2026
The remaining half landed — #4009, merged
1c8354d9fDEFER, and the other one is rejected on evidence. Faulting every in-flight write under a recycling root was measured wrong one lane over in #3112 — the caller's terminal has already fired at
LOCAL_EMITand the 5 s handoff bound is the writer's patience, not a statement about the store, so a fault there reports failure for a write that landed. The NACK arm that is correct — the owner ANSWERING withOwnerDisposing, which the writer re-enqueues — already exists on the update leg and is already green (UpsertAnswersWhenTheOwnerGoesAwayTest).The rule
PackageRootInstallLeases(mesh-scoped singleton).PackageInstaller.Install/InstallNodeRepoDeltaholdTargetPartitionOf(id, manifest)for the whole run;HubRecycleExtensions.RecycleNodeandNodeTypeRebindWatcherconsult it before posting theirDisposeRequest, logging once when they defer (naming the holder) and once when they proceed."Released" always arrives
The lease is taken through
Observable.Using, so it is released on OnCompleted, on OnError and on unsubscribe — the complete set of ways an Rx subscription can end. A failed install releases; an abandoned one releases; the mesh takes the registry with it. No timer force-releases a lease — "defer" has to mean wait for a state that always arrives, and the release arms are pinned by name.Two things deliberately NOT deferred, both to avoid a deadlock
SettleRetypedRoot. It is the holder — its teardown is the install's own ordered act, placed between the twoRequestReleaseswaves by PackageInstaller launches every NodeType compile fire-and-forget, then deliberately disposes the root hub those compiles are reading #1732, andWaitForRootReadyholds the install until the address answers again.What the lease removes is the second, unordered teardown:
NodeTypeRebindWatcherfiring on the placeholder retype this very install performed (#3965 established by code that it can). And because its subscription is registered for disposal on the hub it would recycle, the installer's own recycle cancels it. One recycle per install, not two.Controls, both directions
#3965 measured that a root recycling with no install of its own is the COMMON case, so every regression case is paired with the same composition holding no lease:
ARetypeOfARootAnInstallHolds_DoesNotRecycleUntilTheInstallReleasesItARetypeWithNoInstallHoldingTheRoot_RecyclesAtOnceWhileAnInstallHoldsTheRoot_TheRecycleWaitsAndThenRunsWithNoInstallHoldingTheRoot_TheRecycleProceedsAtOnceTheRootIsHeldWhileTheInstallRuns_AndReleasedWhenItFinishesAnInstallThatFAILS_StillReleasesItsRootWith the three production edits neutralised and both suites rebuilt, exactly those three go red with their own assertion text and every control stays green.
Review found two more holes; both are named rather than papered over
MeshOperations.Recycleposts directly and is NOT gated — an operator recycling a package root mid-install can still strand that install. The class remarks and the doc list every poster outside the gate, because a guard whose reach is assumed reads as a guarantee it does not keep (the defect this issue already corrected once, in the installer's own recycle line).SettleRetypedRootwait for holders that are not its own — is wrong: B'sSettleRetypedRootwould symmetrically wait for A's lease, and two installs each holding and each waiting is a mutual deadlock no timeout may resolve. The sound repair is per-root install serialisation, a separate change.Also fixed in review: the lease key now names the root the install WRITES under (
InstallRootOf), becauseInstallCodefalls back to the sharedtypepartition while the install-record rule falls back to the package id — a lease on<id>for a package writingtype/<id>reads exactly like a lease that is working.What is still open here
This does not make an install immune to every interruption. Per-type recyclers beneath a root are deliberately not deferred, and
SettleRetypedRootstill tears a root down while that root's background pipeline may be issuing correlated requests — the shapeRetypedRootRecycleNeedsAJobTestrecords and #3800 narrowed. What is closed is the class this issue named: nobody but the install itself takes that root down while the install is writing under it.Written up in
Doc/Architecture/DisposalStallVerdicts— extending the page #3965 wrote rather than adding another.rbuergi commentedon Sep 12, 2026
Re-checked 2026-09-12 — #4009's gate is LIVE and measurably firing in production CD; the issue stays open on the half its own source code still names
Two questions were put to this thread today: does the 09-11 SIGSEGV/ALC wave supersede it, and does #4009
finish it. Answers: no and not quite — with numbers for both.
1 — The collectible-unload work does NOT reach this path (family resemblance is not coverage)
#4042/#4046/#4048/#4053 make teardown wait, reactively, for collectible
AssemblyLoadContextunloads. That isthe assembly lifetime, and the wait is consumed in exactly three places, all of them tests:
test/MeshWeaver.Fixture/HubTestBase.cs:155,174,test/MeshWeaver.Hosting.Monolith.TestBase/MonolithMeshTestBase.cs:298,1496,test/MeshWeaver.Fixture/CollectibleUnloadDrain.cs. Insrc/the only references are the DI registration(
Threading/IoPoolServiceCollectionExtensions.cs:47) andCompilationCacheService.RetireInto.PackageInstaller,HubRecycleExtensions,NodeTypeRebindWatcherand the node-ops write path subscribe tonone of it. Different owner (assembly lifetime vs hub lifetime), different teardown, no reach. Nothing in
that wave cancels in-flight work either —
CollectibleUnloadDrainhas no timer and reportsRETAINEDratherthan forcing — so the "teardown lets work finish" constraint is intact on both sides.
2 — #4009 is running in production CD, and the gate fires on every package
Measured on CD run 8399, bake job 103506680455,
2026-09-12 05:44–05:52Z, on image
4c99ec269(= today'smain):[PackageInstaller] recycling root …(SettleRetypedRoot, actually recycling)not recycling root …(#3800's decline on the 0-written re-install)NodeType rebind: deferring the recycle of '<root>' — an install is writing under that root(#4009)[QUIESCE-TIMEOUT] … forcibly cancelling[QUIESCE-START] <root>: N pending callbacks, N ≥ 1[STALE-CALLBACK][PluginGating] reconcile failed … HubDisposedBeforeResponseExceptionUPSERT_CREATE_NO_VERDICT/UPSERT_CREATE_COMPLETED_EMPTYroot … never answered after its recycle[FAIL] … install: TimeoutException(The 62
HubDisposedBeforeResponseExceptionstrings in that log are all inside #3800's rewritten recyclemessage, not exceptions.)
[UpdateQueue] ADVANCE_WITHOUT_HANDOFFstill appears 21× — consistent with the09-08 measurement that ~30 writes miss the 5 s handoff in sealed and failed runs alike; it was never the
discriminator.
Denominator for the recurrence claim: CD runs 8101–8400 (300 runs, 2026-09-08T09:31Z → 2026-09-12T05:27Z)
contain 124 executions of
Bake + publish NodeType assemblies to portal storage. 19 of those failed; all 19logs were fetched and grepped — zero contain
install: TimeoutException, and 15 of the 19 died in job setup with856–943-line logs, i.e. those runs never started a mesh and prove nothing (the other 4 ran the full path,
13.8k–16k lines). So ≥109 executions actually walked the install path with zero occurrences of this shape. Last
recorded occurrence remains CD 7976, 2026-09-07T06:09Z — before #3603 (
fb3eddf74, 09-07 14:20Z) made the createleg total.
3 — One new observability note: 62 deferrals, 0 "the install released" lines
WaitWhileAnInstallHoldsItannounces in both directions, and only the defer half ever printed. That isexplained, not broken:
NodeTypeRebindWatcher.cs:115arms the watcher asinstanceHub.RegisterForDisposal(Arm(...)), so the installer's ownSettleRetypedRootrecycle disposes thewatcher and cancels the deferred recycle — "one recycle per install, not two", exactly as #4009 states.
🚨 The consequence for the next reader: from the log alone, "cancelled by the installer's own recycle"
(intended) and "the lease was never released" (a leak) are indistinguishable — both are silence. A leaked lease
would present as a root that quietly never rebinds, with no line anywhere. If that is ever suspected, the reading
is
PackageRootInstallLeases.HeldBy(<root>)on a live pod, not the bake log.4 — Why it does not close yet: the source itself says half remains
src/MeshWeaver.Mesh.Contract/MeshExtensions.cs:4518, on today'smain, verbatim:Verified rather than taken on trust:
MeshNodeErrorCode.OwnerDisposingis minted on the patch leg only —DataExtensions.cs:908–925(HandlePatchDataRequest,RunLevel > Started⇒ NACK) andRegisterOwnerDisposingNackat:1182, whose parameter isIMessageDelivery<PatchDataRequest>. The CREATE leghas a total bound (
InnerCreateVerdictBound=WriteVerdictBound + 5 s,MeshExtensions.cs:4520) and anamed refusal — so the 10-minute silent hang this issue was filed on is gone — but no owner-side NACK, so where
the update leg re-enqueues against the fresh activation and succeeds, the create leg answers
"the outcome is unknown"and the install fails.And three disposer paths can still take a root while writes are under it, each named by #4009's own review:
PackageInstaller.SettleRetypedRoot— the measured disposer of CD 7950 and of CD 34190841613 — isdeliberately not gated (it is the lease holder). It ran 62× today; its log line now honestly says work in
flight "is given the quiesce window to finish; anything still pending at that bound is force-cancelled".
MeshOperations.Recycleposts directly and consults no lease.separate change (the symmetric fix deadlocks).
Verdict — OPEN, narrowed to one sentence
The class this issue was filed on is closed and now measurably gated: nobody but the install itself takes that
root down while the install is writing under it. What keeps it open is the create leg's owner-side
OwnerDisposingNACK, which the code still annotates as this issue's remaining half, plus the three ungateddisposers above.
What the next occurrence must capture (so nobody re-reads a whole bake log): the trail stage after
UPSERT_READ absent → create—UPSERT_CREATE_NO_VERDICT(the 36 s bound fired: the owner went and nothingNACKed) versus a
nack=OwnerDisposingre-enqueue (the update leg's behaviour, which is what this half wouldgive the create leg) — together with the
[QUIESCE-START] <root>: requested by …line naming the disposer, andwhether a
deferring the recycle of '<root>'line preceded it.Read-only: GitHub REST (no GraphQL),
git show/git greponorigin/main, one bake-job log per failed run.No mesh writes, no cluster access, no test runs.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KFBiMVHLTZM2axNgVvaZN2
rbuergi commentedon Sep 13, 2026
2026-09-13 — unchanged; what #4154 does and does not reach here
Re-read on today's
main: the remaining half is exactly as the 09-12 comment left it —MeshNodeErrorCode.OwnerDisposingis minted on the patch leg only (DataExtensions.csHandlePatchDataRequest/RegisterOwnerDisposingNack), the create leg has its total bound and its named refusal and no owner-side NACK, andSettleRetypedRoot,MeshOperations.Recycleand two packages on one partition remain ungated. Nothing this session merged touches those.Two things from #4154 do reach the shape this issue was filed on, and should be read at the next occurrence rather than the bake log alone: a handler that FAULTS on a package root past
DisposeHostedHubsnow answers its requester (arm 3 used to be dropped byReportFailure's gate), and a NACK the root's parent cannot carry is handed to the requester in-process. Neither is the create leg's owner-sideOwnerDisposingNACK — that is still the open half.🤖 Generated with Claude Code
https://claude.ai/code/session_01BRLdhAiX5DjdZC2WUfPg9E
rbuergi commentedon Sep 14, 2026
2026-09-14 — the attribution arm is now COMPLETE; the create-leg NACK is the whole of what remains
Three things measured on today's
main. Two of them close things this thread has been carrying; the third is unchanged and is now the only open item.1 — Hole 2 of the 09-12 comment is CLOSED, and I ran the pin
That comment listed three ungated disposers. The second — "
MeshOperations.Recycleposts directly and consults no lease" — no longer holds:src/MeshWeaver.Mesh.Operations/MeshOperations.cs:3801—return hub.WhenNoInstallHoldsRoot(resolvedPath), placed AFTER the permission fold and BEFORERecycleCore, so the release-request STAMP (itself a write into the root the install is writing) is gated too, not merely theDisposeRequest.OperatorRecycleWaitsForTheInstallHoldingTheRootTest(Memex.Portal.Shared.Test) andRecycleNodeWaitsForTheInstallHoldingTheRootTest(MeshWeaver.Graph.Test).Memex.Portal.Shared.Testrecycle/install filter and the Graph twin green, on a-c Release -warnaserrorbuild of both projects.So of the three: (1)
SettleRetypedRootis deliberately not gated (it IS the lease holder), (2) is gated, (3) two packages on one partition still needs per-root install serialisation — the symmetric fix deadlocks, as recorded.2 — This issue's third Expectation is now satisfied END TO END, and the last anonymous disposer is gone
[QUIESCE-START]has named it since thedisposeRequestedBy/disposeReason/cascadeOwnerwork. What was still missing is that the line that becomes an ISSUE is not that one.[QUIESCE-START]isInformation; the red-log pipeline filesErrors. #4316 (merged today, for #3712) therefore carries the same attribution onto the[DISPOSE-DISCARD]reports and into the NACK the stranded sender receives — the reader in this issue's own trail, who is in another process and sees none of the root's log.And it removes the one hole in the attribution that nothing else could fill. Measured before the change:
MeshOperations.Recyclewas the only production poster of aDisposeRequestwith noReason, so an operator recycle printed "reason not stated by the caller" — and an operator recycle is precisely the teardown originating OUTSIDE the framework's own lifecycle, i.e. the one a log cannot reconstruct. After:grep -rn "new DisposeRequest()" src --include="*.cs"returns nothing; all 6 of 6 posters insrc/state a reason.For CD 7950's actual question — "who disposed the root is not in the log at this level" — the next occurrence now answers it from the Error tier rather than needing Information-level Loki.
3 — What remains, and it is one sentence
The create leg's owner-side
OwnerDisposingNACK. Unchanged and still annotated as such by the source itself —src/MeshWeaver.Mesh.Contract/MeshExtensions.cs:4595, verbatim:The consequence is the one the 09-12 comment stated: where the update leg re-enqueues against the fresh activation and succeeds, the create leg has only its total bound (
InnerCreateVerdictBound) and answers "the outcome is unknown" — so the install fails on a condition the update leg recovers from. The 10-minute silent hang this issue was filed on is gone; the asymmetry is not.Not attempted here, and stated rather than left implicit:
RegisterOwnerDisposingNackis ~100 lines whose every arm is load-bearing (eager DI capture out of the disposal path, theILatePatchVerdictSinkdirect hand-off that replaced a parent-chain walk measured at ~10× on teardown, the once-only ack gate). Porting it to the create leg is a change of that size and deserves its own diff with its own repro, not a rider on a diagnostics PR.What the next occurrence must capture (unchanged from 09-12, plus one field that now exists): the trail stage after
UPSERT_READ absent → create—UPSERT_CREATE_NO_VERDICTversus anack=OwnerDisposingre-enqueue — together with the[QUIESCE-START] <root>: requested by …line, which the[DISPOSE-DISCARD]/NACK path will now also carry.Read-only for the measurements;
greponorigin/mainin a worktree; the two test filters run locally on-c Release -warnaserrorbuilds. No cluster access, no mesh writes.🤖 Generated with Claude Code
rbuergi commentedon Sep 14, 2026
Triage 2026-09-14: the remaining half is verbatim on main — and the gate #4009/#4202 built is only as wide as the writers that take a lease, of which there is ONE
MeshExtensions.cs:4595still reads "…the remaining half of A package root is disposed while its own install is in flight (CD 7950): the creates' write-through streams lose their owner, no reply is ever sent, and the install runs out its bound — neither #3501 nor #3505 covers it #3510";RegisterOwnerDisposingNack(DataExtensions.cs:1182) isIMessageDelivery<PatchDataRequest>-only;OwnerDisposingis minted only on the update/patch lane;HandleCreateNodeRequest(MeshExtensions.cs:576-1174) has the Rx-completion backstop and noRegisterForDisposalNACK at all. Reader trap: mergedb62521977sits on branchfix/3510-create-leg-owner-disposing-nackbut its diff is theMeshOperations.Recyclegate — the create-leg NACK has not landed.grep -rn "HoldDuring\|leases\.Hold" src→ one production site:MeshWeaver.PluginCatalog/PackageInstaller.cs:195(HoldRootDuringInstall). Every other writer that lands a package tree holds no lease, soNodeTypeRebindWatcher.WaitWhileAnInstallHoldsItandRecycle'sWhenNoInstallHoldsRootfind nobody and proceed: (1) the GitSync importUpdateToLatestFromGitHub(MeshWeaver.GitSync/GitHubActivityExtensions.cs:165; zeroPackageRootInstallLeasesreferences in that project; driven unattended as System fromModuleDiscoveryService.cs:752), and (2) the in-mesh provision pipeline in PluginsStore/Publishing/Source/SystemInstall.cs—CreateRoot→RetypeRoot(aCreateOrUpdateNoderetyping the root toStore/Plugin, exactly whatRequiresRebindfires on) →WireSync→ImportLatest→CompileAllTypes, all unleased (git grep PackageRootInstallLeases origin/mainin Plugins → 0). Established from code, not from a measured occurrence on those paths.Fix sketch: port the owner-side NACK to the create lane (register it from
HandleCreateNodeRequeston its existing once-onlyrespondedgate, eager DI capture, direct verdict hand-off) and re-enqueue the upsert's create arm onOwnerDisposingcapped atMaxOwnerDisposingReenqueues; separately and cheaply, takePackageRootInstallLeases.HoldDuringaroundUpdateToLatestFromGitHuband around the PluginsSystemInstall.Runplan so the existing gates actually see those writers.review(#4391): the fourth import entry point, `Ignored` is a dropped …
rbuergi commentedon Sep 15, 2026
The remaining half landed — #4391, merged
4108aa463f(2026-09-15 09:28Z)That sentence is gone, and so is the thing it named.
Root cause, restated exactly
HandleCreateNodeRequestreturnsProcessed()and owes its reply from a DETACHED chain whose legsreach the partition bootstrap, the validators, storage and the post-creation handlers. A hub disposed
under any of those produces Rx's fourth outcome on that leg — never emits, never completes, never
faults — which neither
DefaultIfEmptynorCatchnor anonCompletedarm can observe. CD 7950'strail ends there:
#3603's
InnerCreateVerdictBoundmade that a named refusal instead of a hang — but a bound expiringis a statement about the caller's patience, not about the write, and it takes 36 s to make it.
What landed
1.
RegisterCreateOwnerDisposingNack— the counterpart ofDataExtensions.RegisterOwnerDisposingNack, registered at handler entry and claiming the handler'sown once-only response gate. Its load-bearing rules are inherited: DI captured on the handler's TURN
(never resolved from the disposal path, which can run past a closed lifetime scope); the verdict
travels through the immediate parent only (walking the chain was measured at ~10× on teardown and
reverted); and no
ILatePatchVerdictSinkanalogue, because that registry servesUpdateRemote'sarmed write watches while a create's waiter is an ordinary
hub.Observecallback that stays armed.2. Claim-then-VERIFY on the verdict post.
respondedis now an interlocked claim, and a refusedpost falls through to the parent — claiming the gate is exactly what disables the NACK (#3196, one
lane over). "Refused" includes
MessageDeliveryState.Ignored, not onlyFailed: the storm breakerreturns it without enqueueing.
3.
CreateNodeResponse.NodeError, and it survives the trip to an exception —NodeCreationFailure.ToExceptionstamps the wholeMeshNodeErrorunderNodeErrorKey, and the NACKnames the disposing hub in its own sentence. The requester is usually in another process and sees
none of the owner's log.
4. The lease's reach.
grep -rn "HoldDuring\|leases\.Hold" srcfound ONE production holder(
PackageInstaller), so #4009/#4202's gate could not see the GitSync import — which writes a wholeSpace tree including the
NodeTyperetypesRequiresRebindfires on. All four import entrypoints now hold (
UpdateToLatest,UpdateToProvenCommit,ReconcileAtProvenCommit,Reimport);CommitToGitHubdeliberately does not, since it writes no mesh nodes.🚨 What is NOT in it, and why — a withdrawal on evidence
An automatic re-drive was written twice and is not shipped. Neither site can keep the promise:
address, so the only activation whose disposal can mint that NACK for it is this hub — and a hub
past
DisposeHostedHubscannot take a re-drive. Unreachable code that looks like a recovery readsto the next person as a covered case.
MeshService.CreateNodewas implemented and driven against the rig, and it does not work. NodeCRUD executes on
portal/nodeops-{meshId}, a stream-ROUTED address that is unregistered with itshub and is not re-materialised by posting to it: the re-post is answered
RouteMessage: NotFound … No node found at 'portal/nodeops-…'. The retry buys one extra round tripbefore the identical failure — a bound-widening band-aid.
So the create leg gets a verdict, not a recovery. That is this issue's own Expected, verbatim:
"the recycle answers every in-flight write under it with a NACK … so the install fails in
milliseconds with a name instead of running out a 10-minute bound." Making that address re-form is a
separate change, and the measurement is recorded in
Doc/Architecture/WriteVerdictTotalityso nobody re-derives it.
Measured, and the test failed first
CreateAnswersWhenTheOwnerGoesAwayTestparks a realINodeValidatorinside the create chain — theseam the create actually traverses, after this thread's two recorded attempts to park a partition
root's primary stream failed because "the root's
Versionnever advances after the post, so thecreate does not route through the parked hub at all."
IMeshService.CreateNodeException.Data, hub namedThree cases, plus
GitSyncImportHoldsTheSpaceRootTest(all three release arms, the no-registrypass-through, and that a sibling Space is not held — the mis-keying #4009's own review caught).
Suites:
MeshWeaver.Graph.Test1642/1642,MeshWeaver.Documentation.Test473/473,MeshWeaver.Hosting.Test718/718, on-c Release -warnaserror.Review found three things, all valid; two were misses
ReimportFromGitHub— the settings tab's manual re-import — held no lease, and was missed because itis the one import that does not go through
TriggerAuthorizedAsSystem. I pattern-matched thewriters instead of enumerating them, which is precisely the defect the section I wrote names. The
Ignoreddelivery state was the second. The third asked what pinned the re-drive; the answer was"nothing could", and it is withdrawn above.
What stays open, elsewhere
(
CreateRoot→RetypeRoot→WireSync→ImportLatest→CompileAllTypes) holds no leaseeither.
git grep PackageRootInstallLeasesin that repo returns 0. Established from code, not froma measured occurrence.
and the symmetric fix deadlocks. Per-root install serialisation is its own change.
Closing this one: every Expectation it states is now met — the disposer is named, the recycle is
deferred while an install holds the root, and the in-flight create is answered with a NACK instead of
running out a bound.
🤖 Generated with Claude Code