Repository navigation
GUI file-upload does not auto-index: FileBrowser never raises the IContentUploadObserver seam #170
Description
Summary
A file uploaded through the portal file-browser GUI (the FileBrowser Blazor component, e.g. the /{node}/Files page) is written to the content collection but is never indexed. The auto-index-on-upload seam is only raised by the MCP/AI upload path (MeshOperations.Upload), not by the GUI upload path. Agent instructions and the pipeline documentation state that "an upload auto-indexes" — that is true only for the MCP path, so the GUI path silently violates the documented contract.
This is a missing call at the true source, not a pipeline defect — the fix is to raise the existing seam on the GUI path (root cause), not to add a re-index workaround.
Impact
- Files uploaded via the GUI are not searchable (0 chunks, no
Documentnode) until a manual whole-collection Re-Index is run. - The behaviour is inconsistent and surprising: MCP-uploaded files index automatically; GUI-uploaded files do not.
- Observed on
client-iAgenticPension/content:Nest_GB25_DE_final.pdf(GUI-uploaded) had 0 chunks; several other root-level files (Nest_2023.pdf,Nest_2024.pdf) are likewise unindexed, while MCP-uploaded files (contract_motor_2025.pdf,PKG_2024.docx) are indexed.
Reproduction
- Open a content collection in the portal file browser (
FileBrowser//collections/{collection}/ a node'sFilespage). - Upload a
.pdfor.docx. - Observe: the file appears in the browser, but no
Index {file}Activity is created andGetChunkCount(collectionPath, file) == 0. - Upload the same file through the MCP
uploadtool (orMeshOperations.Upload): anIndex {file}Activity fires and chunks are stored.
Root cause
The post-upload seam is hub.RaiseContentUploaded(collectionPath, filePath) (src/MeshWeaver.ContentCollections/IContentUploadObserver.cs). It fans out to every registered IContentUploadObserver; the indexing reactor ContentIndexingObserver.OnUploaded (src/MeshWeaver.ContentCollections.Indexing.Graph/ContentIndexingObserver.cs:109) fires the index Activity.
The seam is raised from exactly one call site:
src/MeshWeaver.AI/MeshOperations.cs:1391-1398— saves the file and then raises the seam:await collection.SaveFileAsync(dir, fileName, ms).ConfigureAwait(false); hub.RaiseContentUploaded(qualifiedCollectionName, filePath); // ← triggers auto-index
The GUI upload handler saves the file but never raises the seam:
src/MeshWeaver.Blazor/FileExplorer/FileBrowser.razor.cs:225(OnFileUploadedAsync):await Collection.SaveFileAsync(CurrentPath!, file.Name, file.Stream!); progressPercent = 100; ToastService.ShowSuccess($"File {file.Name} successfully uploaded."); // no RaiseContentUploaded → no observer → no auto-index
The pipeline doc comment already encodes the assumption that uploads flow through the MCP path only: "an upload through the standard content-upload path (MeshOperations.Upload → ContentCollection.SaveFileAsync) raises IContentUploadObserver" (ContentIndexingPipelineExtensions.cs). The GUI is a second, parallel upload path that was never wired to the seam.
Proposed fix
Primary (minimal, root cause)
Raise the seam in FileBrowser.OnFileUploadedAsync immediately after a successful save, mirroring MeshOperations.Upload. FileBrowser already injects IMessageHub Hub.
await Collection.SaveFileAsync(CurrentPath!, file.Name, file.Stream!);
// Post-upload seam: notify registered observers (content indexing, …) AFTER the save
// succeeds — mirrors MeshOperations.Upload. Synchronous, fire-and-forget: the observer
// returns immediately and does its work off-band in an Activity (ControlledIoPooling).
var relativePath = string.IsNullOrEmpty(CurrentPath) || CurrentPath == "/"
? file.Name
: $"{CurrentPath.Trim('/')}/{file.Name}";
Hub.RaiseContentUploaded(QualifiedCollectionPath, relativePath);
progressPercent = 100;Correctness note — the seam needs the QUALIFIED path. RaiseContentUploaded → ContentIndexingObserver.IndexFileActivity → RegisterCollection requires a {node}/{collection} path and throws Collection path '…' must be '{node}/{collection}' on a bare name. MeshOperations passes qualifiedCollectionName = $"{resolution.Prefix}/{collectionName}". The FileBrowser.CollectionName parameter and URL form (/collections/{CollectionName}, '~'→'/' via DecodeCollectionName) can be an encoded/bare name, so the fix must construct the qualified path — decode '~'→'/', and combine with the owning node (Address) when CollectionName is not already {node}/{collection}. Verify against the AgenticPension/content case (CollectionName should resolve to AgenticPension/content).
Recommended hardening (prevent recurrence)
Two upload entry points exist (MCP and GUI) and one forgot the seam — a third could too. Extract "save a user-uploaded file + raise the seam" into a single shared helper and route both MeshOperations.Upload and FileBrowser through it, so no upload site can omit the seam:
// e.g. an extension in MeshWeaver.ContentCollections
public static async Task SaveUploadedFileAsync(
this IContentCollection collection, IMessageHub hub,
string qualifiedCollectionPath, string dir, string fileName, Stream content, CancellationToken ct = default)
{
await collection.SaveFileAsync(dir, fileName, content, ct).ConfigureAwait(false);
var filePath = string.IsNullOrEmpty(dir) ? fileName : $"{dir.Trim('/')}/{fileName}";
hub.RaiseContentUploaded(qualifiedCollectionPath, filePath);
}This aligns with the "use one shared path, do not hand-roll a parallel version" principle. Treat the primary fix as the immediate correctness fix and the helper as the follow-up that removes the divergence.
Guideline compliance
- No band-aid — the fix is the missing seam call at the source, not a re-index watchdog/poller or a "Re-Index all" workaround (root-cause only).
- Blazor async —
RaiseContentUploadedis a synchronousvoidfire-and-forget call; the observer immediately returns and runs indexing off-band via an Activity on the FileSystem I/O-pool. Noawait/.ToTask()is added, so BlazorAsync and ControlledIoPooling are respected. The pre-existingawait Collection.SaveFileAsync(...)(a stream-provider IO call) is unchanged and out of scope. - No new hand-rolled async/concurrency — reuses the framework seam only.
Testing (per WritingTests — no mocking)
Extend test/MeshWeaver.ContentCollections.Indexing.Graph.Test/ContentIndexingPipelineTest.cs:
- Regression at the seam: after the GUI-equivalent sequence (
collection.SaveFileAsync(...)thenhub.RaiseContentUploaded(qualifiedPath, file)), assert anIndex {file}Activity reaches terminalSucceededandstore.GetChunkCount(collectionPath, file) > 0— waited on viastream.Where(...).FirstAsync().Timeout(...), neverTask.Delay. - Path qualification: assert
RaiseContentUploadedwith a bare collection name is rejected/qualified as intended (guard the{node}/{collection}contract). - If the hardening helper is adopted, unit-test
SaveUploadedFileAsyncdirectly (save happened AND seam raised) so any upload path routed through it is covered by construction. (bUnit component-level testing ofFileBrowseris not established in the repo; prefer the seam/helper-level integration test.)
Out of scope
- GitSync-synced files also bypass the seam (a different caller). Auto-indexing an entire repo sync can be costly; the intended path for bulk is the settings-tab whole-collection Re-Index (
ContentIndexingObserver.ReindexAll). Track separately if desired. - Rewriting the existing
await SaveFileAsyncupload flow.
References
src/MeshWeaver.ContentCollections/IContentUploadObserver.cs— the seam +RaiseContentUploadedsrc/MeshWeaver.AI/MeshOperations.cs:1391-1398— the one caller that raises itsrc/MeshWeaver.Blazor/FileExplorer/FileBrowser.razor.cs:216-235— GUI handler missing the raisesrc/MeshWeaver.ContentCollections.Indexing.Graph/ContentIndexingObserver.cs— the indexing reactorsrc/MeshWeaver.ContentCollections.Indexing.Graph/ContentIndexingPipelineExtensions.cs— pipeline wiring + "standard upload path" assumption- Docs: ControlledIoPooling, BlazorAsync, AsynchronousCalls, WritingTests
Activity
rbuergi commentedon Jul 2, 2026
PR #189 fixes the FileBrowser GUI path (the reported case). The same investigation found two more save paths that never raise the seam — noting them here rather than widening that PR:
FileContentProvider.SaveFileContent(src/MeshWeaver.ContentCollections/FileContentProvider.cs— used byAIExtensions.HandleSaveContentfor AI text saves): saves viacollection.SaveFileAsyncwithout raising the observer.ContentImportExtensions.CopyAsync(bulkImportContentRequestcopies): each imported file is saved without raising the observer — bulk-imported content stays unindexed until a manual re-index.Both need the same one-line
RaiseContentUploadedat their save points (the import loop needs the hub threaded in).