From 92f44b0ac2bda49d131a9b8ca8f9c5c80d312d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Tue, 30 Jun 2026 21:17:41 +0200 Subject: [PATCH 1/3] fix(ai): bundle built-in AI content sources so a partition can't be silently un-imported (Skill) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The static-repo import sources for the built-in AI content (Agent / Provider / Harness / Skill) were registered one-by-one in the portal and gated on a hand-maintained per-partition allow-list (StaticRepoSync.Partitions). Forgetting a name — as happened with Skill — left that partition served from in-memory while the rest went to the DB: no import, silently. The user has hit this repeatedly. Treat AI content as ONE bundle, owned by MeshWeaver.AI: - AiContentSources.ContentPartitions — the canonical AI content partition set (Agent/Provider/ Harness/Skill), next to the sources. - AiContentSources.AddBuiltInAiContentSources() — registers EVERY built-in AI IStaticRepoSource. - StaticRepoSyncExtensions registers the whole bundle whenever any AI partition is served (no per-name list to forget); MemexConfiguration expands the served set to the whole bundle if it names any AI partition, so AddAI's per-type serve-from-DB gating stays consistent with the import. Ratchet: AiContentSourcesTest reflects over every IStaticRepoSource in MeshWeaver.AI and fails if any is not in the bundle — so a NEW AI content type can never again be silently left un-imported. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Memex.Portal.Shared/MemexConfiguration.cs | 6 +++ .../StaticRepoSyncExtensions.cs | 20 ++++---- src/MeshWeaver.AI/AiContentSources.cs | 46 +++++++++++++++++++ .../AiContentSourcesTest.cs | 45 ++++++++++++++++++ 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/MeshWeaver.AI/AiContentSources.cs create mode 100644 test/MeshWeaver.AI.Test/AiContentSourcesTest.cs diff --git a/memex/Memex.Portal.Shared/MemexConfiguration.cs b/memex/Memex.Portal.Shared/MemexConfiguration.cs index 5942139bee..30438f1768 100644 --- a/memex/Memex.Portal.Shared/MemexConfiguration.cs +++ b/memex/Memex.Portal.Shared/MemexConfiguration.cs @@ -480,6 +480,12 @@ public TBuilder ConfigureMemexMesh(IConfiguration configuration, bool isDevelopm // import — no regression. Default Helm sets ["Doc","Agent","Provider","Harness","Skill"]. var syncPartitions = features.StaticRepoSync.Partitions .ToHashSet(StringComparer.OrdinalIgnoreCase); + // AI content is served as a UNIT: if the config names ANY AI partition, serve them ALL + // (Agent/Provider/Harness/Skill), so an incomplete list can't leave Skill (or a future AI + // content type) in-memory while the rest go to the DB — and AddAI's per-type serve-from-DB + // gating stays consistent with the static-repo import. See MeshWeaver.AI/AiContentSources. + if (syncPartitions.Overlaps(AiContentSources.ContentPartitions)) + syncPartitions.UnionWith(AiContentSources.ContentPartitions); IReadOnlySet serveFromPartition = syncPartitions; MeshBuilder mb = builder diff --git a/memex/Memex.Portal.Shared/StaticRepoSyncExtensions.cs b/memex/Memex.Portal.Shared/StaticRepoSyncExtensions.cs index 4d8877fd7a..d9df592db3 100644 --- a/memex/Memex.Portal.Shared/StaticRepoSyncExtensions.cs +++ b/memex/Memex.Portal.Shared/StaticRepoSyncExtensions.cs @@ -37,16 +37,16 @@ public static TBuilder AddStaticRepoSync(this TBuilder builder, { if (serveFromPartition.Contains("Doc")) services.AddSingleton(); - if (serveFromPartition.Contains("Agent")) - services.AddSingleton(); - // The model catalog (providers + models + policy) imports into the top-level "Provider" - // partition. Honour the legacy "Model" partition name too for backwards-compatible configs. - if (serveFromPartition.Contains("Provider") || serveFromPartition.Contains("Model")) - services.AddSingleton(); - if (serveFromPartition.Contains("Harness")) - services.AddSingleton(); - if (serveFromPartition.Contains("Skill")) - services.AddSingleton(); + + // AI content (Agent / Provider / Harness / Skill) is ONE bundle: register every built-in + // AI source together whenever any AI partition is served, so a config that names only some + // of them can NEVER silently drop one (the recurring "Skill was never imported" bug). The + // set + the bundle live next to the sources in MeshWeaver.AI — adding a new AI content type + // there auto-joins the import; there is no per-partition allow-list here to forget. (The + // expand-to-the-whole-bundle also happens in MemexConfiguration so AddAI's serve-from-DB + // gating stays consistent with the import.) + if (serveFromPartition.Overlaps(AiContentSources.ContentPartitions)) + services.AddBuiltInAiContentSources(); // Runs after the PG schema-provisioning hosted service (registered earlier by // AddPartitionedPostgreSqlPersistence) — hosted services start in registration order. diff --git a/src/MeshWeaver.AI/AiContentSources.cs b/src/MeshWeaver.AI/AiContentSources.cs new file mode 100644 index 0000000000..a16ccf522a --- /dev/null +++ b/src/MeshWeaver.AI/AiContentSources.cs @@ -0,0 +1,46 @@ +using MeshWeaver.Mesh; +using Microsoft.Extensions.DependencyInjection; + +namespace MeshWeaver.AI; + +/// +/// The built-in AI content partitions and their static-repo import sources, treated as ONE bundle. +/// AI content — Agents, model Providers, Harnesses, Skills — is seeded from these sources on boot. +/// Bundling them is the cure for the recurring "Skill partition was never imported" bug: there is no +/// per-partition allow-list to hand-maintain, so a new built-in AI content type can't be silently +/// left un-imported by forgetting to name its partition. This is the single source of truth shared by +/// AddAI's serve-from-DB gating and the portal's static-repo import wiring. +/// +/// Pinned by AiContentSourcesTest: every defined in +/// MeshWeaver.AI MUST be in — adding a fifth source +/// without bundling it fails the test. +/// +public static class AiContentSources +{ + /// + /// The partitions whose content is the built-in AI catalog: Agent, Provider, + /// Harness, Skill. Served from the DB (and imported) as a UNIT — never partially. + /// + public static readonly IReadOnlySet ContentPartitions = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "Agent", // AgentStaticRepoSource.Partition + ModelProviderNodeType.RootNamespace, // "Provider" + HarnessNodeType.RootNamespace, // "Harness" + SkillNodeType.RootNamespace, // "Skill" + }; + + /// + /// Registers EVERY built-in AI (Agent, Model/Provider, Harness, + /// Skill) as one bundle. The portal calls this whenever AI content is served from the DB, so the + /// import set can never drift from or silently drop a partition. + /// + public static IServiceCollection AddBuiltInAiContentSources(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } +} diff --git a/test/MeshWeaver.AI.Test/AiContentSourcesTest.cs b/test/MeshWeaver.AI.Test/AiContentSourcesTest.cs new file mode 100644 index 0000000000..41c29e4391 --- /dev/null +++ b/test/MeshWeaver.AI.Test/AiContentSourcesTest.cs @@ -0,0 +1,45 @@ +using System.Linq; +using MeshWeaver.Mesh; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace MeshWeaver.AI.Test; + +/// +/// The ratchet behind the recurring "Skill partition was never imported" bug: every built-in AI +/// defined in MeshWeaver.AI MUST be in the +/// bundle. Adding a new AI content type +/// (a fifth partition) without bundling it fails this test — so it can never again be silently left +/// un-imported by an incomplete per-partition list. +/// +public class AiContentSourcesTest +{ + [Fact] + public void AddBuiltInAiContentSources_bundles_every_AI_static_repo_source() + { + // Reflect over EVERY concrete IStaticRepoSource in the MeshWeaver.AI assembly. + var allAiSources = typeof(AiContentSources).Assembly.GetTypes() + .Where(t => t is { IsAbstract: false, IsClass: true } + && typeof(IStaticRepoSource).IsAssignableFrom(t)) + .ToHashSet(); + allAiSources.Should().NotBeEmpty("MeshWeaver.AI defines built-in static-repo content sources"); + + var registered = new ServiceCollection() + .AddBuiltInAiContentSources() + .Where(sd => sd.ServiceType == typeof(IStaticRepoSource)) + .Select(sd => sd.ImplementationType!) + .ToHashSet(); + + var unbundled = allAiSources.Except(registered).OrderBy(t => t.Name).ToList(); + unbundled.Should().BeEmpty( + "AddBuiltInAiContentSources must register EVERY IStaticRepoSource in MeshWeaver.AI — " + + "bundling them is what stops a new AI partition (Skill, …) from being silently un-imported"); + } + + [Fact] + public void ContentPartitions_cover_the_four_AI_content_partitions() + { + AiContentSources.ContentPartitions.OrderBy(p => p, System.StringComparer.Ordinal) + .Should().Equal("Agent", "Harness", "Provider", "Skill"); + } +} From 72681dad2ddc92f01c015e3b10bde497dfafd9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Tue, 30 Jun 2026 21:26:12 +0200 Subject: [PATCH 2/3] fix(ai): ContentPartitions is an ImmutableHashSet (repo immutable-collections policy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address Copilot: a static readonly HashSet is mutable; use ImmutableHashSet for the partition constant. (The Overlaps comment is moot — IReadOnlySet defines Overlaps; the portal builds clean.) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MeshWeaver.AI/AiContentSources.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/MeshWeaver.AI/AiContentSources.cs b/src/MeshWeaver.AI/AiContentSources.cs index a16ccf522a..36d76d71f9 100644 --- a/src/MeshWeaver.AI/AiContentSources.cs +++ b/src/MeshWeaver.AI/AiContentSources.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using MeshWeaver.Mesh; using Microsoft.Extensions.DependencyInjection; @@ -21,14 +22,13 @@ public static class AiContentSources /// The partitions whose content is the built-in AI catalog: Agent, Provider, /// Harness, Skill. Served from the DB (and imported) as a UNIT — never partially. /// - public static readonly IReadOnlySet ContentPartitions = - new HashSet(StringComparer.OrdinalIgnoreCase) - { + public static readonly ImmutableHashSet ContentPartitions = + ImmutableHashSet.Create( + StringComparer.OrdinalIgnoreCase, "Agent", // AgentStaticRepoSource.Partition ModelProviderNodeType.RootNamespace, // "Provider" HarnessNodeType.RootNamespace, // "Harness" - SkillNodeType.RootNamespace, // "Skill" - }; + SkillNodeType.RootNamespace); // "Skill" /// /// Registers EVERY built-in AI (Agent, Model/Provider, Harness, From 6d2badf13eeea769fd7603f9ec84da0bcc72c1da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= Date: Tue, 30 Jun 2026 21:43:29 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix(ai):=20set=20Icon=20on=20Agent/Provider?= =?UTF-8?q?/Skill=20partition=20roots=20(#132=20=E2=80=94=20blank=20cards)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Agent/Provider/Skill PartitionRoot definitions never set an Icon, so on every distributed portal those space roots render as blank cards in search/catalogs. Set them (sparkle/database/sparkle), matching Doc (organization) and Harness (bot). The content change bumps the source fingerprint, so the next boot re-imports the root with the icon — no manual SQL. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/MeshWeaver.AI/AgentStaticRepoSource.cs | 1 + src/MeshWeaver.AI/ModelStaticRepoSource.cs | 1 + src/MeshWeaver.AI/SkillStaticRepoSource.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/MeshWeaver.AI/AgentStaticRepoSource.cs b/src/MeshWeaver.AI/AgentStaticRepoSource.cs index da8c039211..a9881707b5 100644 --- a/src/MeshWeaver.AI/AgentStaticRepoSource.cs +++ b/src/MeshWeaver.AI/AgentStaticRepoSource.cs @@ -38,6 +38,7 @@ public IReadOnlyList EnumerateSourceNodes() => public MeshNode? PartitionRoot => new("Agent") { Name = "Agents", + Icon = "/static/NodeTypeIcons/sparkle.svg", NodeType = "Space", State = MeshNodeState.Active, Content = new MarkdownContent diff --git a/src/MeshWeaver.AI/ModelStaticRepoSource.cs b/src/MeshWeaver.AI/ModelStaticRepoSource.cs index e4068ca2c9..06d32b9429 100644 --- a/src/MeshWeaver.AI/ModelStaticRepoSource.cs +++ b/src/MeshWeaver.AI/ModelStaticRepoSource.cs @@ -39,6 +39,7 @@ public IReadOnlyList EnumerateSourceNodes() => public MeshNode? PartitionRoot => new(ModelProviderNodeType.RootNamespace) { Name = "Providers", + Icon = "/static/NodeTypeIcons/database.svg", NodeType = "Space", State = MeshNodeState.Active, Content = new MarkdownContent diff --git a/src/MeshWeaver.AI/SkillStaticRepoSource.cs b/src/MeshWeaver.AI/SkillStaticRepoSource.cs index a2d14acdfc..d5f6e71b3a 100644 --- a/src/MeshWeaver.AI/SkillStaticRepoSource.cs +++ b/src/MeshWeaver.AI/SkillStaticRepoSource.cs @@ -35,6 +35,7 @@ public IReadOnlyList EnumerateSourceNodes() => public MeshNode? PartitionRoot => new(SkillNodeType.RootNamespace) { Name = "Skills", + Icon = "/static/NodeTypeIcons/sparkle.svg", NodeType = "Space", State = MeshNodeState.Active, Content = new MarkdownContent