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);
}
}
}