Repository navigation
Description
Split out of #2361, whose installer-side defect is fixed by #2402. This is the other half: something writes a Skill/presentation (and Skill/slide) row at Version=1, carrying what looks like Publish/Skill/presentation's instructions and no Category/Order, while the Skill package is installing.
The evidence
Two MeshWeaver.Plugins gate runs on 2026-08-26 (job 98168018140 on run 32965818521, and publish-bake job 98177992724 on run 32968600070) carry this inside the Skill package's FIRST install, ~330 ms after ── Skill: installing 23 file(s)…:
[MonotonicWriteGuard] CONFLICT on Skill/presentation: a write at Version=0 lost the race against the durable Version=1.
Resolved by merging into the durable row: merged=(none); latest-wins (stale values DROPPED)=Category, Order, Content.instructions.
[MonotonicWriteGuard] CONFLICT on Skill/slide: … (identical)
The write at Version=0 is the installer's bulk write of the package's own node. So a durable row at Version=1 already existed, and it is not the package's node.
MeshNodeConflictMerge reports exactly the members that diverged, which pins the shape of that row:
Name,Description,Icon,NodeTypeare NOT reported ⇒ they match the package's node (/presentation,Presentation,Skill).CategoryandOrderARE reported ⇒ they differ. All three authored copies in the repo (Skill/,Publish/Skill/,Essentials/Skill/) carry identicalcategory: Skillsandorder: 17/16, so the durable row carries neither — it looks like they were LOST somewhere, not set to something else.Content.instructionsIS reported, i.e. the two strings are not resolvable byMeshNodePatchMerge.TryMergeTwoWay's single-splice superset rule. Of the three copies only Publish's has that property againstSkill's:Essentials/Skill/presentation's instructions are a clean single-splice subset ofSkill/presentation's (they would have MERGED and been reported undermerged=), while Publish's diverges in the middle as well (PublishAndDecksvsSlidesAndDecks).
Publish installs immediately before Skill in the gate's dependency order, and presentation + slide are EXACTLY the two node ids Publish/Skill/ and Skill/ have in common — which is also exactly the pair the gate reports, every time.
What it is NOT
- Not two packages declaring one path.
PackageInstaller.ParseCanonicalderives the node path from the file's repo-relative location, soPublish/Skill/presentation.json→Publish/Skill/presentation. - Not
BuiltInSkillProvider. The tester boots.AddAI(AiContentSources.ContentPartitions), soAddSkillTypetakes its dbSynced branch and registers no static node provider forSkill; itsIConfigurationis empty, so noSkillStaticRepoSourceimport is registered either.
Not reproduced locally
Three full 53-package gate runs against the plugins checkout (one at DOTNET_PROCESSOR_COUNT=4), each with a probe printing every InMemoryStorageAdapter.Write for any path containing Skill/presentation, saw exactly ONE write per path — the install's own. The extra writer did not appear on this host.
Suggested next diagnostic
The cheapest next step is to make the conflict say WHAT the durable row is: MonotonicWriteGuardStorageAdapter.ResolveConflict already has latest in hand, so logging its Name/Category/NodeType (and a short content fingerprint) alongside the member list would identify the writer from the next CI occurrence without a repro. The guard also writes an ActivityLog satellite at {path}/_Activity/write-conflict-{version} — that node already carries the account and is another place to look on a portal where this happens.
Activity
rbuergi commentedon Aug 26, 2026
The writer is identified:
Store/Publishing's pre-installed-skill MIRROR, in MeshWeaver.PluginsSkill/presentationandSkill/slideatVersion=1are written byStore/Publishing/Source/PreInstall.cs— in-mesh C# in the MeshWeaver.Plugins node repo, so it is invisible to
dotnet build, to anygrep --include='*.cs'over this repo, and to every hypothesis this issue and #2361 tested.A plugin declaring
preInstalled: truehas its shipped{plugin}/Skill/*mirrored into the globalSkill/registry (the namespace the composer's canonical skill query reads for every user).PluginGating.csdrives it from the plugin root hub —DelaySubscription+Throttle(2s)— andSyncGlobalSkillswrites the plan throughmesh.CreateNodeundersystem-security.That accounts for every attribute the guard reported, with nothing left over:
Version=1CreateNodeon a brand-new path mintsMeshNode.NextVersion(0)= 1CategorydivergedMirror()does not copyCategory— the mirror's is null, the package's is"Skills"OrderdivergedOrderis null, the package's17/16Name/Description/Icon/NodeTypeNOT reportedMirror()copies all four verbatimContent.instructionsunmergeablePublish's body, which diverges from the master's mid-stringpresentation+slidePublish/Skill/and theSkillpackage have in commonSkillpackage's install is a race with the gate's sequential install progressThe proof, not the inference
1. The durable content is byte-for-byte
Publish's, notEssentials' and not core'scontent/ai/Skill/*.md. Plugins CI run32996370339, withMW_INSTALL_DIFF=1armed by MeshWeaver.Plugins@3ddf7727, prints the canonical content signaturelengths:
Recomputing
ContentSignatureover every candidate file (canonical JSON, .NET default escaping)resolves it uniquely — the two-character
PublishAndDecksvsSlidesAndDecksdifference is exactlywhat separates the winner from the runners-up:
presentationslideSkill/*.json(the master — the incoming side, 7138 / 10300)Publish/Skill/*.jsonEssentials/Skill/*.jsoncontent/ai/Skill/*.md(pre-preamble)2. The mirror was caught in the act, locally, with a probe on
InMemoryStorageAdapter.Write(path + scalars + stack) and on
HandleCreateNodeRequest(sender). A full 53-package gate runagainst the plugins checkout shows the mirrors as a distinct population —
v1,cat='',order=,ns='Skill'— beside the packages' ownv0rows:and the create-side probe names the poster:
Every mirrored id belongs to a
preInstalled: trueplugin (Collaboration, DataModelling, Edu,Feedback, Hosting, Observability, Publish); no id from a non-pre-installed one (Providers,
RemoteControl, RolePlay, Store, Training, Video) is mirrored. The bursts land tens of seconds after
their own package's install — which is the
DelaySubscription+Throttle— and that lateness isthe race.
Three local runs stayed GREEN for the same reason the CI runs went red: locally the
Publishmirrorfired after the
Skillpackage had installed, soMirrorPlansaw a FOREIGN occupant atSkill/presentationand correctly left it alone (IsOwnMirroris false — itsMainNodeis its ownpath). In CI it fired first, created the row, and the package's
Version=0bulk write then lost toit exactly as #2361 described.
What this was NOT (each ruled out with evidence, so nobody re-tests them)
BuiltInSkillProvider/ a static node provider. The gate boots.AddAI(AiContentSources.ContentPartitions), soAddSkillTypetakes its dbSynced branch andregisters neither the
IStaticNodeProvidernor theStaticNodePartitionStorageProvider; theprobe run shows no static write at those paths. And the pinned image (
b2e1ff9, ACR3.0.0-rc8.ci.5285) contains Skill catalog: port the write-into-your-own-home preamble to the canonical /presentation + /slide seeds #2187, so itscontent/ai/Skill/presentation.mdbody is byte-equalto the master package's — it could not have produced a divergent
Content.instructionsat all.NodeFileMapper.FromRelativePathruns on the REPO-relative path(
NodeRepoPackageSource.FetchPackageFileskeeps thePublish/prefix), soPublish/Skill/presentation.json→Publish/Skill/presentation, confirmed by the trace above.version the repo file carries, which is correct.
Status and follow-ups
21 duplicate skill copies —
Publish/Skill/presentation.jsonandslide.jsonamong them — andadded
check_one_skill_mastertoscripts/validate-repos.py, which refuses any package otherthan
Skill/re-shipping a master id. That is the right resolution: one master per platformskill, gated.
_Activity/write-conflict-{n}record now carry thewinning row's
MainNode. AmainNodepointing outside the path's own partition names across-partition mirror on sight — which is the one line that would have ended this hunt
immediately. (
LastModifiedBycannot: every system-impersonated writer stampssystem-security.)PreInstall.MirrordropsCategoryandOrder, so every mirrored skill currently sits in the global registry with no category andorder 0 — visible in the trace above for
Skill/multipart-doc,Skill/thread,Skill/ui-extensibilityand the rest.MirrorEqualsdoes not compare them either, so the loss isnot self-healing. Worth fixing there; it does not affect this issue's outcome.
rbuergi commentedon Aug 26, 2026
Follow-ups now tracked:
MainNodein the conflict warning and in the_Activity/write-conflict-{n}trace, so a cross-partition mirror is named on sight. Pinned by a test that fails on main.PreInstall.Mirror(dropsCategory+Order, andMirrorEqualsdoes not compare them, so it never heals).check_one_skill_master).Review: the warning states the INVARIANT, not a cause it cannot know