Skip to content

fix(#354): boot self-heal asserts served AI catalog partitions are search-indexed - #524

Merged
rbuergi merged 2 commits into
mainfrom
fix/354-ai-catalog-index-selfheal
Jul 18, 2026
Merged

rbuergi merged 2 commits into
mainfrom
fix/354-ai-catalog-index-selfheal

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Fixes the live residual of #354 (the config drift was already fixed by #407; this adds the boot-time self-heal/assert that served AI catalog partitions are provisioned + indexed).

The residual

On affected instances the global AI catalog partitions (Provider/Agent/Skill/Harness) are materialized but missing from the search index: exact-path get @Provider/Anthropic works, but every index-based access (search nodeType:ModelProvider, @Provider/* children listing, the AI-menu catalog areas) returns empty. #407 fixed the config-alias/allow-list drift that stopped the catalogs being imported. What remained: nothing at boot asserted that a served catalog actually landed in public.searchable_schemas.

Root cause of the residual: the static-repo import's own queries are all partition-pinned, so they never trigger a SyncSearchableSchemasAsync. A freshly-provisioned catalog schema therefore stays absent from searchable_schemas until the first unpinned fan-out query happens to run one — until then the cross-schema UNION skips it and index queries return 0 forever.

Fix (additive, idempotent, reactive)

  • StaticRepoImporter.ImportAll now takes the served catalog set and, after the import, runs AssertCatalogPartitionsIndexed: for each catalog partition actually backed by a registered source this run (candidate ∩ current sources — an in-memory-served or non-served catalog is left alone), it forces one searchable_schemas rebuild (bypassing the query-hot-path throttle — this is a one-time boot action, not a per-query rebuild), reads the registry back, and surfaces a startup-failure notification (existing NotifyStartupFailure path) for any served catalog still absent. So an already-broken instance self-repairs on restart and the failure is visible. No-op on in-memory backends (no ICrossSchemaQueryProvider). PG round-trips run on the IIoPool; no Observable.FromAsync, no await.
  • ICrossSchemaQueryProvider gains SyncSearchableSchemasAsync(bool force, ct); the PostgreSQL + Snowflake implementations bypass the TTL when force=true (single-flight mutual-exclusion is still honoured). Existing callers are unchanged (the old (ct) overload delegates with force:false).
  • Portal boot passes AiContentSources.ContentPartitions as the assertion set.

Test

test/MeshWeaver.Hosting.PostgreSql.Test/CatalogPartitionIndexSelfHealTests (PG Testcontainers, pgvector/pgvector:pg17):

  1. after ImportAll, the served catalog is registered in searchable_schemas and a cross-schema search finds its seeded ModelProvider/Skill nodes;
  2. revert-proven: a searchable_schemas row that later goes missing (the "materialized but unindexed" drift) is re-registered by the next boot's self-heal even inside the 30 s throttle window — the force=true bypass is exactly what makes that deterministic (a plain throttled sync would skip and leave it gone).

Ran locally via Testcontainers — 2/2 green. Release -warnaserror -p:CIRun=true build of every touched project is clean (the only diagnostics are the pre-existing repo-wide NU1902 AngleSharp advisory).

🤖 Generated with Claude Code

…arch-indexed

The config-alias/allow-list drift that made the global AI catalogs
(Provider/Agent/Skill/Harness) render empty was fixed by #407. This closes
the live RESIDUAL: nothing at boot asserted that a served catalog partition
actually landed in the cross-schema search index (public.searchable_schemas).

Because the static-repo import's own queries are all partition-PINNED, they
never trigger a SyncSearchableSchemasAsync, so a freshly-provisioned catalog
schema stays absent from searchable_schemas until the first unpinned fan-out
query happens to run one. Meanwhile every index query (search
nodeType:ModelProvider, children listing, the catalog areas) returns empty
although exact-path get works — "materialized but unindexed".

Fix (additive, idempotent, reactive):
- StaticRepoImporter.ImportAll takes the served catalog set and, after the
  import, runs AssertCatalogPartitionsIndexed: for each catalog actually
  backed by a registered source this run, FORCE one searchable_schemas
  rebuild (bypassing the query-hot-path throttle — a one-time boot action,
  never per-query) so a just-provisioned schema is indexed immediately, then
  read the registry back and surface a startup-failure notification (existing
  NotifyStartupFailure path) for any served catalog still absent. In-memory
  backends and non-served catalogs are left untouched. PG round-trips run on
  the IIoPool; no Observable.FromAsync, no await.
- ICrossSchemaQueryProvider gains SyncSearchableSchemasAsync(bool force, ct);
  the PG + Snowflake impls bypass the TTL when force=true (single-flight is
  still honoured). Existing callers are unchanged.
- Portal boot passes AiContentSources.ContentPartitions as the assertion set.

Test (PG Testcontainers): CatalogPartitionIndexSelfHealTests pins that a
served catalog is registered in searchable_schemas + a cross-schema search
finds its seeded nodes, and a revert-proven case where a dropped
searchable_schemas row is re-registered by the next boot's self-heal even
inside the throttle window (the force=true bypass is what makes it
deterministic). Ran locally, 2/2 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

   60 files  + 1     60 suites  +1   29m 38s ⏱️ +53s
6 857 tests +61  6 554 ✅ +61  303 💤 ±0  0 ❌ ±0 
6 912 runs  +61  6 609 ✅ +61  303 💤 ±0  0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

♻️ This comment has been updated with latest results.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

887 tests  ±0   881 ✅ ±0   4m 35s ⏱️ -9s
  4 suites ±0     6 💤 ±0 
  4 files   ±0     0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

@github-actions

github-actions Bot commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

696 tests  ±0   592 ✅ ±0   5m 38s ⏱️ -19s
 10 suites ±0   104 💤 ±0 
 10 files   ±0     0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

♻️ This comment has been updated with latest results.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

766 tests  ±0   765 ✅ ±0   5m 32s ⏱️ ±0s
 12 suites ±0     1 💤 ±0 
 12 files   ±0     0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 259 tests  ±0   1 255 ✅ ±0   5m 4s ⏱️ +2s
   11 suites ±0       4 💤 ±0 
   11 files   ±0       0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files  + 1     11 suites  +1   4m 10s ⏱️ + 1m 11s
1 065 tests +59  1 065 ✅ +59  0 💤 ±0  0 ❌ ±0 
1 120 runs  +59  1 120 ✅ +59  0 💤 ±0  0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

2 184 tests  +2   1 996 ✅ +2   4m 36s ⏱️ +8s
   12 suites ±0     188 💤 ±0 
   12 files   ±0       0 ❌ ±0 

Results for commit 01c5acf. ± Comparison against base commit 87a264b.

@rbuergi
rbuergi merged commit 0288e2c into main Jul 18, 2026
21 of 23 checks passed
@rbuergi
rbuergi deleted the fix/354-ai-catalog-index-selfheal branch August 5, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant