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