using System.Text.Json.Nodes; using Nexus.Api.Repositories; namespace Nexus.Api.Services; /// /// Re-establishes the official OpenClaw session subscriptions after every /// Gateway reconnect and subscribes active durable Nexus runs to sanitized /// message/tool/approval events. /// public sealed class OpenClawEventSubscriptionCoordinator( IGatewayConnector connector, IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService { private static readonly TimeSpan SyncInterval = TimeSpan.FromSeconds(2); private readonly Dictionary _messageSubscriptions = new(StringComparer.Ordinal); private DateTimeOffset? _connectionEpoch; private bool _sessionCatalogSubscribed; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { await SynchronizeOnceAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception exception) { logger.LogWarning( exception, "Could not synchronize OpenClaw event subscriptions; retrying."); } await Task.Delay(SyncInterval, stoppingToken); } } public async Task SynchronizeOnceAsync(CancellationToken cancellationToken = default) { if (connector.ConnectionState != GatewayConnectionState.Connected) { ResetConnectionState(); return; } var connectionEpoch = connector.LastConnectedAt; if (_connectionEpoch != connectionEpoch) { ResetConnectionState(); _connectionEpoch = connectionEpoch; } if (!_sessionCatalogSubscribed && connector.Supports("sessions.subscribe")) { await connector.InvokeAsync( "sessions.subscribe", new JsonObject(), cancellationToken: cancellationToken); _sessionCatalogSubscribed = true; } if (!connector.Supports("sessions.messages.subscribe")) return; await using var scope = scopeFactory.CreateAsyncScope(); var repository = scope.ServiceProvider.GetRequiredService(); var activeRuns = await repository.GetActiveSubscriptionsAsync(cancellationToken); var desiredSubscriptions = activeRuns.ToDictionary( BuildSubscriptionKey, subscription => subscription, StringComparer.Ordinal); var includeApprovals = connector.GrantedScopes.Contains("operator.admin") || connector.GrantedScopes.Contains("operator.approvals"); foreach (var activeRun in activeRuns) { var subscriptionKey = BuildSubscriptionKey(activeRun); if (_messageSubscriptions.ContainsKey(subscriptionKey)) continue; var parameters = new JsonObject { ["key"] = activeRun.SessionKey, ["agentId"] = activeRun.AgentId }; if (includeApprovals) parameters["includeApprovals"] = true; await connector.InvokeAsync( "sessions.messages.subscribe", parameters, cancellationToken: cancellationToken); _messageSubscriptions[subscriptionKey] = activeRun; } if (connector.Supports("sessions.messages.unsubscribe")) { foreach (var obsolete in _messageSubscriptions .Where(item => !desiredSubscriptions.ContainsKey(item.Key)) .ToList()) { await connector.InvokeAsync( "sessions.messages.unsubscribe", new JsonObject { ["key"] = obsolete.Value.SessionKey, ["agentId"] = obsolete.Value.AgentId }, cancellationToken: cancellationToken); _messageSubscriptions.Remove(obsolete.Key); } } } private void ResetConnectionState() { _connectionEpoch = null; _sessionCatalogSubscribed = false; _messageSubscriptions.Clear(); } private static string BuildSubscriptionKey(OpenClawRunSubscription subscription) => $"{subscription.SessionKey}\u001f{subscription.AgentId}"; }