Repository navigation
delete-space leaves an empty same-named Space behind: the drain re-provisions the partition it just removed, so a removal cannot be verified by listing #3436
Description
Symptom
delete-space drains a Space and then an EMPTY shell of it reappears within milliseconds —
a bare Space node plus a _Policy (PartitionAccessPolicy) child, and nothing else.
Measured on the systemorph staff portal, 2026-09-06, from the OperationRequest
rbuergi/Requests/remove-reinsurance-and-education (state: Succeeded, Done: 68 step(s) ran.,
startedAt 10:55:23.79, finishedAt 10:55:50.90).
Four of the nine course Spaces the run deleted are back:
| Space | delete step | Space createdDate | _Policy createdDate |
was, per the run's own preview |
|---|---|---|---|---|
AgenticPrimerDe |
59/68 | 10:55:37.568 | 10:55:37.635 | Store/Plugin "Agentic Primer — Wünsch dir Software!", 59 nodes |
DataImportExport |
62/68 | 10:55:38.914 | — | Store/Plugin "Data Import & Export", 44 nodes |
DataModeling |
65/68 | 10:55:49.688 | — | Store/Plugin "Data Modelling", 429 nodes |
ThinkInStreams |
68/68 | 10:55:52.892 | 10:55:53.005 | Store/Plugin "Think in Streams", 90 nodes |
Every one of these logged deleted in the run log. ThinkInStreams is the cleanest evidence that
this is not a within-run ordering artifact: it was the LAST step, and its re-creation timestamp is
2 seconds after the run's own finishedAt.
createdBy / lastModifiedBy on all of them is system-security, version: 1, state: Active.
What makes this a defect and not "the content came back"
The content did NOT come back — 429 + 90 + 59 + 44 nodes are genuinely gone. What returns is a
DIFFERENT node from the one deleted:
- nodeType changed:
Store/Plugin→Space - name changed: the display name ("Agentic Primer — Wünsch dir Software!") → the bare path
segment (AgenticPrimerDe) - exactly one child:
_Policy, created 67 ms after the root
So a delete leaves behind an empty, differently-typed shell that looks like a Space in every listing
and is invisible as damage. A nodeType:Space sweep after the run reports these four as present and
healthy.
Hypothesis (NOT established — this needs the code read)
The drain appears to trigger partition provisioning on the partition it is removing:
EnsurePartitionProvisioned writes the default PartitionAccessPolicy, and writing that child
materialises its parent Space root. The 67 ms gap between root and _Policy, and the tight coupling
of each re-creation to its own delete step, both fit — but the ORDER (does the policy write create
the root, or does a root re-create trigger the policy?) is inferred from timestamps, not read from
the code. Treat it as a lead, not a finding.
Not the mechanism, ruled out by measurement:
- Module discovery / AutoSync re-provisioning from a registry source. The request's own
reason
field warned about exactly this, so it was the obvious suspect — but there is no
Plugins/AgenticPrimerDe,Plugins/DataImportExport,Plugins/DataModelingor
Plugins/ThinkInStreamspackage. All 66Plugins/*packages were enumerated; none of the four is
among them. (Plugins/DataModelling, double-L, is a different package and was not deleted.) - A surviving
_GitSync. Steps 57, 60, 63 and 66 each loggeddeletedfor these. - Leftover content re-materialising the root. Each returned Space has exactly one descendant,
the_Policy, and it postdates the root.
The open puzzle worth explaining before fixing
Five of the nine did NOT come back: AdvancedBusinessRules, AgenticBusiness,
AgenticEngineering, AgenticOffice, and — the awkward one — AgenticPrimer, which had 57 nodes
and a deleted _GitSync, i.e. the closest possible sibling to AgenticPrimerDe (59 nodes, same
shape) which DID come back.
Whatever the mechanism is, it must explain that asymmetry. A theory that predicts all nine should
return is wrong. Do not stop at a fix that makes the symptom go away without accounting for the five.
Why it matters beyond tidiness
The maintainer's policy is that the Systemorph staff portal carries Plugins + Crm only. A removal
that silently leaves a same-named shell behind means:
- the policy cannot be verified by listing Spaces — the listing lies;
- a subsequent removal run reports the Space as existing and deletes it again, indefinitely;
- anything that keys off partition existence (access checks, provisioning, module discovery) sees a
partition that was supposed to be gone.
Repro
Delete any Space with content via SpaceOps.Delete() on a portal, then read the path back. Expect
absent; observe a bare Space + _Policy. 🚨 Do this on a disposable mesh, not on a staff portal.
Fix constraints
- 🚨 Root cause only. A post-delete sweep that deletes the shell again is a band-aid and would race
the same provisioning. - 🚨 Provisioning is REACTIVE + POOLED (
EnsurePartitionProvisioned(namespace).SelectMany(…)).
Never declare aPartitionDefinitionnode to force a schema, and never lowercase a namespace by
hand. - The delete path must make the partition's removal and its provisioning mutually exclusive in a
modelled way — not by ordering luck and not by a delay. - A regression test must assert the path is ABSENT after the drain settles, and must be falsified by
reverting the fix and watching it red.
Provenance
Found while executing the maintainer's decision to cut the re-provisioning source before re-running
the SocialMedia/Manufacturing removal. Blocks that work: re-running a removal against this defect
would produce four more shells.
Related: the still-unrun request rbuergi/Requests/remove-socialmedia (requestedAction: Preview,
steps: [], no Script child) is blocked separately on MeshWeaver.Plugins#1320.
Activity
rbuergi commentedon Sep 6, 2026
Root cause established: the drop handler is registered PER NodeType, and only for two of them
PartitionDropPostDeletionHandlermatches on a single NodeType string supplied at its registrationsite:
It is constructed in exactly two places in
src/:src/MeshWeaver.Graph/Configuration/SpaceNodeType.cs:202src/MeshWeaver.Graph/Configuration/UserNodeType.cs:57The four partitions that survived were rooted at
Store/Pluginnodes, notSpaceones — fromthe run's own preview details:
Every partition that was correctly and completely removed — the whole Reinsurance family plus
AdvancedBusinessRulesandAgenticEngineering— was rooted at aSpace. Confirmed againstAdmin/Partition/*(106 entries): none of the Reinsurance partitions remain, and exactly these fourdo.
So the handler never ran for a
Store/Plugin-rooted partition. The class's own doc commentpredicts this exact failure for a different type:
Store/Pluginis the third type. The fix stated there was never generalised — it was applied twice,by hand, at two registration sites. The registration should be driven by
OwnsPartitionbeingtrue, not by a hand-maintained list of two, and a guard should assert that every
OwnsPartitionNodeType has a teardown handler, so a fourth type cannot repeat this.
This also corrects my earlier hypothesis in the issue body: partition provisioning racing the drain
is NOT the mechanism. The drop simply never happened, and the empty shell is what a later
provisioning left behind on a partition that was still fully alive underneath.
It also explains the asymmetry the issue asked about
AgenticPrimer(57 nodes) wasStore/Plugintoo and did NOT come back, whileAgenticPrimerDe(59nodes) did — so nodeType alone does not predict the SHELL. But it does predict the SCHEMA, which is
the part that matters: whether a visible shell reappears depends on whether anything subsequently
touched the partition, while the orphaned schema is there either way. A shell is the visible
subset of the damage; absence of a shell is not evidence the partition was dropped. Any audit must
read
Admin/Partition/*and the schema list, never the Space listing.Third symptom: the orphan is self-protecting
Attempting the cleanup through the sanctioned API as the maintainer's own identity:
…for all four. The re-provisioned partition carries a fresh default
PartitionAccessPolicy(
AgenticPrimerDe/_Policy, created 67 ms after the root) which does not grant Delete to the identitythat deleted the original. So the orphan cannot be cleaned up through the same path that created it.
That makes the severity higher than "leftover shell": a partition can reach a state where it is
undeletable by ordinary means and invisible in a Space listing.
Scope of the live damage on the systemorph staff portal
Four orphaned partitions, all still registered in
Admin/Partition:AgenticPrimerDeDataImportExportThinkInStreamsDataModelingDataModelingis the clearest case: its definition predates the removal entirely, so the deletenever touched it.
🚨 Do not confuse it with
DataModelling(double L, "Data Modelling"), which is a LIVE package andpartition and must not be touched.
Worth a separate look, not part of this issue
The same listing shows several probable historical orphans that no current Space accounts for —
BinaryClicker,BinaryClickerV2,binary-clicker,binary-clicker-space(four variants of onething),
BusinessModelsbesideBusinessModel,DocumentationbesideDoc,ProvidersbesideProvider,RFPProcessor,UWDeepfield,UWDeepfieldData. These are consistent with the samedefect operating over months. An audit that reconciles
Admin/Partition/*against live partitionroots would size it;
V03_DropRogueSchemasis the precedent for the cleanup shape.doc(#3436): point the residual-race note at its tracking issue
test(#3436): use TestTimeouts instead of literal 30 s waits
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-09-06)
Fix in #3449 — and one correction to the proposed generalisation
The mechanism in the comment above is confirmed. The proposed fix is not sufficient, though, and
that turned out to be the load-bearing detail.
Driving the registration off
NodeTypeDefinition.OwnsPartition == truewould still have missed allfour victims:
Store/Pluginis declared in mesh CONTENT, so nosrc/-side scan can enumerate it — as theissue says. But also:
Store/Plugindoes not setOwnsPartitionat all. Read off the live node(
get @Store/Plugin, systemorph): itsNodeTypeDefinitioncarries noownsPartitionfield, andthe serializer omits defaults. It does not need one —
PackageInstaller.EnsurePartitionsProvisionedprovisions the target partition, not
OwnsPartitionProvisioningValidator. So partition creationhas two triggers and only one of them consults
OwnsPartition; a runtime scan for it would haveskipped
Store/Plugintoo.So #3449 keys the teardown on the only predicate with no blind spot — the shape of the deleted
node. A partition is a first path segment, so a top-level node IS its partition's root, whatever its
type and whenever that type arrived (
PartitionDefinition.IsPartitionRoot). Registered once, byAddGraph. Details, both guards and the falsification are in the PR and inDoc/Architecture/PartitionTeardown.The empty shell, explained
EnsurePartitionBootstrap→ProvisionAndCreateRoot: a child write into a partition whose ROOT rowis missing provisions the schema and writes a generic
Spaceroot named after the path segment.That is exactly the observed shape (
Store/Plugin→Space, display name → bare id,_Policy67 ms later), and it is a second trigger for partition creation behind
OwnsPartitionProvisioningValidator's back. The writer was almost certainlyInstalledPackageRepairService, which re-assertsPackageInstaller.EnsureDeclaredAccessfor everyrecorded install — and the install record lives in the
Pluginspartition, so deleting theinstalled partition never removes it.
That also explains the asymmetry the issue asked about: the shell appears only where something
subsequently wrote into the partition. #3449 gates the absent-root branch on the partition's backing
store not being definitively gone, which closes the steady-state loop. It does not close a child
write that lands while the delete is still draining — see #3451.
The orphan audit (read-only; nothing on systemorph was changed)
Reconciled
Admin/Partition/*(106) against live partition roots. 🚨 The root set is not justSpace— three in-mesh root NodeTypes are in play on this portal:Store/PluginSpacesrc/Crm/ClientStore/CatalogStore)Every candidate was then confirmed with an exact-path read (
get @{partition}), not with thequery alone:
scope:childrenlistings are partition-scoped and RLS-filtered —Adminitself doesnot appear in an unscoped
nodeType:Space scope:childreneven though its root exists — so a query'scount: 0is not by itself evidence of absence.21 partition definitions have no live root:
BinaryClickerBinaryClickerV2binary-clickerbinary-clicker-spaceBusinessModelsDocumentationRFPProcessorUWDeepfieldUWDeepfieldDataClientAlphaTestEmailEAAHelvetiaMusicInsPQTechnologySchenkerLabsValucorVHVGruppeLouisBraunRobMindStefanBerneggerEvery suspect named in the issue is in that list, and the shape of the residue matches the defect:
the four
binary-clicker*variants,BusinessModelsbeside the liveBusinessModel,Documentationbeside the live
Doc, and three user-home-shaped names (LouisBraun,RobMind,StefanBernegger)which fit the pre-2026-07-19
Userhalf of the same bug.Also worth a look, the converse: 9 live roots have no partition definition —
AI,Crm,LinkedIn,Manufacturing,Marketing,SocialMedia,Video,X,YouTube(plusClientA,a live
Crm/Client). Harmless in itself (a definition is not required to route), but it means theAdmin/Partitionlisting is not a complete index of partitions in either direction.And the four from this issue now exist as empty
Spaceshells rather than as missing roots:AgenticPrimerDe,DataImportExport,DataModeling,ThinkInStreams.What the audit could not do, and what would finish it
The authoritative third list is the schemas themselves —
SELECT nspname FROM pg_namespace ORDER BY 1on the portal database. A definition can be missing while the schema is there, and vice versa, so
until that is read the 21 above are candidates, not a confirmed schema census. That needs a DB
read I deliberately did not perform.
No cleanup was run and none is proposed here.
V03_DropRogueSchemas(MeshWeaver.Pluginssrc/Memex.Database.Migration/Migrations/) is the precedent for the shape if a migration iswarranted; the procedure is written up in
Doc/Architecture/PartitionTeardown. #3449 only stops NEWorphans.