diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Configuration.cs b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Configuration.cs
new file mode 100644
index 00000000..7514a6ab
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Configuration.cs
@@ -0,0 +1,127 @@
+using AIStudio.Tools.Databases.IndexStore;
+
+using Microsoft.AspNetCore.Components;
+
+using static AIStudio.Tools.Services.Indexing.IndexingLogFormat;
+
+namespace AIStudio.Tools.Services;
+
+///
+/// Changes to the embedding settings which do not pass through the queue: the configuration of an
+/// organization, which may change an embedding provider whenever its plugin is applied, and the
+/// confidence level of a provider, which is set for all of its uses at once.
+///
+public sealed partial class DataSourceEmbeddingService
+{
+ #region Implementation of IMessageBusReceiver
+
+ public Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
+ {
+ if (triggeredEvent is not (Event.PLUGINS_RELOADED or Event.CONFIGURATION_CHANGED))
+ return Task.CompletedTask;
+
+ //
+ // Not awaited: the plugins start, and the settings save, while this may wait for the index
+ // database.
+ //
+ var queueIdleDataSources = triggeredEvent is Event.PLUGINS_RELOADED;
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await this.StopOutdatedRunsAsync();
+ if (queueIdleDataSources)
+ await this.QueueOutdatedIdleDataSourcesAsync();
+ }
+ catch (Exception exception)
+ {
+ logger.LogWarning(exception, "Failed to check the data sources against the changed configuration.");
+ }
+ });
+
+ return Task.CompletedTask;
+ }
+
+ public Task> ProcessMessageWithResult(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) => Task.FromResult(MessageBusAnswer.NONE);
+
+ #endregion
+
+ ///
+ /// Stops every active run which works for embedding settings which no longer apply.
+ ///
+ ///
+ /// Reads no database, because it runs on every change of a setting anywhere in the app. A run
+ /// which has not recorded its signature yet checks for itself once it does.
+ ///
+ private async Task StopOutdatedRunsAsync()
+ {
+ foreach (var (dataSourceId, activeRun) in this.activeRuns)
+ {
+ // Stopping already, e.g. because the change was saved through the queue:
+ if (activeRun.TokenSource.IsCancellationRequested)
+ continue;
+
+ DataSourceRunSignature? runSignature;
+ lock (this.runSignatureLock)
+ runSignature = activeRun.RunSignature;
+
+ if (IsRunOutdated(runSignature, this.GetCurrentRunSignature(dataSourceId)))
+ await this.StopOutdatedDataSourceRunAsync(dataSourceId);
+ }
+ }
+
+ ///
+ /// Queues every data source at rest whose stored index belongs to embedding settings which no longer apply.
+ ///
+ ///
+ /// Such a data source stays out of the selection until its index is built anew, cf.
+ /// IsAwaitingReindexAsync, which so far happened only once one of its documents changed or the
+ /// app started again. The rebuild is the one the next run would do anyway, decided by the same
+ /// signature. It only comes sooner, and only while the data sources refresh on their own.
+ ///
+ /// A data source with a run, or with one queued, is left alone: StopOutdatedRunsAsync takes care
+ /// of the run, and a queued run reads the current settings when it starts. The same holds for the
+ /// data sources the startup check has yet to reach, which wait as queued as well.
+ ///
+ private async Task QueueOutdatedIdleDataSourcesAsync()
+ {
+ if (!settingsManager.ConfigurationData.App.DataSourceIndexing.AutomaticRefresh)
+ return;
+
+ // The startup check has yet to come, and it reads the current settings for every data source:
+ if (Volatile.Read(ref this.startupHashCheckStarted) == 0)
+ return;
+
+ IndexStoreClient? indexStore = null;
+ foreach (var dataSource in this.GetConfiguredIndexedSources())
+ {
+ if (this.activeRuns.ContainsKey(dataSource.Id))
+ continue;
+
+ if (this.statuses.TryGetValue(dataSource.Id, out var status) && status.State is DataSourceEmbeddingState.QUEUED or DataSourceEmbeddingState.RUNNING)
+ continue;
+
+ // No run would start, and its status says why already:
+ var currentRunSignature = this.GetCurrentRunSignature(dataSource.Id);
+ if (currentRunSignature == DataSourceRunSignature.NONE)
+ continue;
+
+ indexStore ??= await databaseClientProvider.GetIndexStoreAsync(CancellationToken.None);
+ if (!indexStore.IsAvailable)
+ return;
+
+ var indexState = await indexStore.GetDataSourceStateAsync(dataSource.Id, CancellationToken.None);
+ if (string.Equals(indexState?.EmbeddingSignature, currentRunSignature.EmbeddingSignature, StringComparison.Ordinal))
+ continue;
+
+ logger.LogInformation(
+ "Queueing data source '{DataSourceName}' ({DataSourceId}) because the changed configuration changed its embedding settings. StoredSignaturePrefix={StoredSignaturePrefix}, CurrentSignaturePrefix={CurrentSignaturePrefix}.",
+ dataSource.Name,
+ dataSource.Id,
+ ShortHash(indexState?.EmbeddingSignature ?? string.Empty),
+ ShortHash(currentRunSignature.EmbeddingSignature));
+
+ await this.QueueDataSourceAsync(dataSource);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Signature.cs b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Signature.cs
index dda783c3..06d3c1f0 100644
--- a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Signature.cs
+++ b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.Signature.cs
@@ -1,5 +1,6 @@
using System.Globalization;
+using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Mail;
@@ -140,4 +141,40 @@ public sealed partial class DataSourceEmbeddingService
/// The signature of this pairing.
internal static string BuildEmbeddingSignature(IDataSourceBase dataSource, EmbeddingProvider embeddingProvider) =>
BuildEmbeddingSignature(dataSource, embeddingProvider, GetChunkingOptions(dataSource, embeddingProvider));
+
+ ///
+ /// Describes the indexing run a data source would get with a given embedding provider.
+ ///
+ ///
+ /// The embedding signature, as long as the provider may read the data source at all. Comparing
+ /// what a run started with against what it would start with now tells whether the run is
+ /// outdated: either it embeds for an index the next run throws away, or it sends documents to a
+ /// provider which may no longer read them.
+ ///
+ /// The data source to index.
+ /// The embedding provider the data source names.
+ /// The confidence level of that embedding provider.
+ /// The signature of the run, or NONE when the provider may not read the data source and no run would start.
+ internal static DataSourceRunSignature GetRunSignature(IIndexedDataSource dataSource, EmbeddingProvider embeddingProvider, ConfidenceLevel embeddingProviderConfidence) =>
+ AllowsEmbedding(dataSource, embeddingProviderConfidence)
+ ? new(BuildEmbeddingSignature(dataSource, embeddingProvider))
+ : DataSourceRunSignature.NONE;
+
+ ///
+ /// Whether an indexing run works for embedding settings which no longer apply.
+ ///
+ /// The signature the run started with, or null while the run has not resolved its embedding provider yet.
+ /// The signature a run would start with now, or NONE when no run would start anymore.
+ /// True when the run should stop.
+ internal static bool IsRunOutdated(DataSourceRunSignature? runSignature, DataSourceRunSignature currentRunSignature)
+ {
+ //
+ // Still preparing, so nothing was embedded or sent anywhere yet. The run checks for itself
+ // once it records its signature.
+ //
+ if (runSignature is null)
+ return false;
+
+ return currentRunSignature == DataSourceRunSignature.NONE || runSignature != currentRunSignature;
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs
index 6ea6be21..22e464f6 100644
--- a/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs
+++ b/app/MindWork AI Studio/Tools/Services/DataSourceEmbeddingService.cs
@@ -17,7 +17,7 @@ using static AIStudio.Tools.Services.Indexing.IndexingLogFormat;
namespace AIStudio.Tools.Services;
public sealed partial class DataSourceEmbeddingService(SettingsManager settingsManager, RustService rustService, DatabaseClientProvider databaseClientProvider,
- PromptInjectionGuardService guardService, ILogger logger) : BackgroundService
+ PromptInjectionGuardService guardService, ILogger logger) : BackgroundService, IMessageBusReceiver
{
///
/// How long the re-index check waits for the index database before it gives up.
@@ -40,6 +40,12 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
private readonly ConcurrentDictionary activeRuns = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary statuses = new(StringComparer.OrdinalIgnoreCase);
private readonly object queueStateLock = new();
+
+ ///
+ /// Guards the run signature of every active run, cf. DataSourceRunControl.RunSignature.
+ ///
+ private readonly Lock runSignatureLock = new();
+
private int startupHashCheckStarted;
private int startupHashCheckCompleted;
@@ -55,7 +61,17 @@ public sealed partial class DataSourceEmbeddingService(SettingsManager settingsM
private sealed record DataSourceEmbeddingQueueItem(string DataSourceId, DataSourceEmbeddingRefreshMode RefreshMode);
- private sealed record DataSourceRunControl(CancellationTokenSource TokenSource, TaskCompletionSource