namespace Nexus.Api.Services; /// /// Continuously folds bounded Gateway chat/run events into the durable Nexus /// run projection. Re-processing is safe because per-run sequence cursors and /// persisted terminal/gap event ids reject duplicates. /// public sealed class OpenClawRunEventReconciler( IGatewayConnector connector, IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(750); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { string? cursor = null; while (!stoppingToken.IsCancellationRequested) { try { var snapshot = connector.GetRecentEvents(500) .OrderBy(item => item.ReceivedAt) .ThenBy(item => item.Sequence) .ThenBy(OpenClawEventIdentity.Create, StringComparer.Ordinal) .ToList(); var startIndex = cursor is null ? 0 : snapshot.FindIndex(item => string.Equals( OpenClawEventIdentity.Create(item), cursor, StringComparison.Ordinal)) + 1; if (startIndex <= 0 && cursor is not null) { // The bounded buffer rolled over. Re-process what remains; // run-level sequence cursors and persisted event ids dedupe it. startIndex = 0; } if (startIndex < snapshot.Count) { await using var scope = scopeFactory.CreateAsyncScope(); var runs = scope.ServiceProvider.GetRequiredService(); foreach (var gatewayEvent in snapshot.Skip(startIndex)) { await runs.ReconcileAsync(gatewayEvent, stoppingToken); cursor = OpenClawEventIdentity.Create(gatewayEvent); } } else if (snapshot.Count > 0) { cursor = OpenClawEventIdentity.Create(snapshot[^1]); } } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception exception) { logger.LogWarning( exception, "Could not reconcile the latest OpenClaw run events; retrying without dropping the durable projection."); } await Task.Delay(PollInterval, stoppingToken); } } }