namespace Nexus.Api.Services; public sealed class AgentProvisioningOptions { public const string SectionName = "OpenClawAgentProvisioning"; /// /// OpenClaw-host path, not a Nexus container mount. The Gateway resolves /// the leading tilde when agents.create is executed. /// public string WorkspaceRoot { get; set; } = "~/.openclaw"; public int PollIntervalSeconds { get; set; } = 5; public int LeaseSeconds { get; set; } = 60; public int MaxProposalFileBytes { get; set; } = 262_144; public int MaxProposalFilesBytes { get; set; } = 524_288; } /// /// Bounded wake-up signal. PostgreSQL remains authoritative; losing this /// process-local signal merely falls back to polling. /// public sealed class AgentProvisioningSignal { private readonly System.Threading.Channels.Channel channel = System.Threading.Channels.Channel.CreateBounded( new System.Threading.Channels.BoundedChannelOptions(1) { FullMode = System.Threading.Channels.BoundedChannelFullMode.DropWrite, SingleReader = true, SingleWriter = false }); public void Notify() => channel.Writer.TryWrite(true); public async Task WaitAsync(TimeSpan maximumDelay, CancellationToken cancellationToken) { using var delayCancellation = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken); var signal = channel.Reader.WaitToReadAsync(cancellationToken).AsTask(); var delay = Task.Delay(maximumDelay, delayCancellation.Token); var completed = await Task.WhenAny(signal, delay); if (completed == signal && await signal) { while (channel.Reader.TryRead(out _)) { } } delayCancellation.Cancel(); } } public sealed class AgentProvisioningWorker( IServiceScopeFactory scopeFactory, AgentProvisioningSignal signal, Microsoft.Extensions.Options.IOptions options, ILogger logger) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await using (var startupScope = scopeFactory.CreateAsyncScope()) { try { await startupScope.ServiceProvider .GetRequiredService() .RecoverInterruptedRequestsAsync(stoppingToken); } catch (Exception exception) when (!stoppingToken.IsCancellationRequested) { logger.LogError( exception, "Agent provisioning recovery failed; live mutations remain paused until the next poll"); } } var delay = TimeSpan.FromSeconds(Math.Clamp( options.Value.PollIntervalSeconds, 1, 60)); while (!stoppingToken.IsCancellationRequested) { var processed = false; try { await using var scope = scopeFactory.CreateAsyncScope(); processed = await scope.ServiceProvider .GetRequiredService() .ProcessNextAsync(stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception exception) { logger.LogError(exception, "Agent provisioning worker iteration failed"); } if (!processed) await signal.WaitAsync(delay, stoppingToken); } } }