Skip to content

GUI file-upload does not auto-index: FileBrowser never raises the IContentUploadObserver seam #170

Closed

Description

@sierragolflima

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 Document node) 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-i AgenticPension/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

  1. Open a content collection in the portal file browser (FileBrowser / /collections/{collection} / a node's Files page).
  2. Upload a .pdf or .docx.
  3. Observe: the file appears in the browser, but no Index {file} Activity is created and GetChunkCount(collectionPath, file) == 0.
  4. Upload the same file through the MCP upload tool (or MeshOperations.Upload): an Index {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 — RaiseContentUploaded is a synchronous void fire-and-forget call; the observer immediately returns and runs indexing off-band via an Activity on the FileSystem I/O-pool. No await/.ToTask() is added, so BlazorAsync and ControlledIoPooling are respected. The pre-existing await 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:

  1. Regression at the seam: after the GUI-equivalent sequence (collection.SaveFileAsync(...) then hub.RaiseContentUploaded(qualifiedPath, file)), assert an Index {file} Activity reaches terminal Succeeded and store.GetChunkCount(collectionPath, file) > 0 — waited on via stream.Where(...).FirstAsync().Timeout(...), never Task.Delay.
  2. Path qualification: assert RaiseContentUploaded with a bare collection name is rejected/qualified as intended (guard the {node}/{collection} contract).
  3. If the hardening helper is adopted, unit-test SaveUploadedFileAsync directly (save happened AND seam raised) so any upload path routed through it is covered by construction. (bUnit component-level testing of FileBrowser is 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 SaveFileAsync upload flow.

References

  • src/MeshWeaver.ContentCollections/IContentUploadObserver.cs — the seam + RaiseContentUploaded
  • src/MeshWeaver.AI/MeshOperations.cs:1391-1398 — the one caller that raises it
  • src/MeshWeaver.Blazor/FileExplorer/FileBrowser.razor.cs:216-235 — GUI handler missing the raise
  • src/MeshWeaver.ContentCollections.Indexing.Graph/ContentIndexingObserver.cs — the indexing reactor
  • src/MeshWeaver.ContentCollections.Indexing.Graph/ContentIndexingPipelineExtensions.cs — pipeline wiring + "standard upload path" assumption
  • Docs: ControlledIoPooling, BlazorAsync, AsynchronousCalls, WritingTests

Activity

  1. rbuergi commented on Jul 2, 2026

    @rbuergi
    Contributor

    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:

    1. FileContentProvider.SaveFileContent (src/MeshWeaver.ContentCollections/FileContentProvider.cs — used by AIExtensions.HandleSaveContent for AI text saves): saves via collection.SaveFileAsync without raising the observer.
    2. ContentImportExtensions.CopyAsync (bulk ImportContentRequest copies): 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 RaiseContentUploaded at their save points (the import loop needs the hub threaded in).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinguifront-end

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions