106 lines
3.7 KiB
C#
106 lines
3.7 KiB
C#
namespace Nexus.Api.Services;
|
|
|
|
public sealed class AgentProvisioningOptions
|
|
{
|
|
public const string SectionName = "OpenClawAgentProvisioning";
|
|
|
|
/// <summary>
|
|
/// OpenClaw-host path, not a Nexus container mount. The Gateway resolves
|
|
/// the leading tilde when agents.create is executed.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bounded wake-up signal. PostgreSQL remains authoritative; losing this
|
|
/// process-local signal merely falls back to polling.
|
|
/// </summary>
|
|
public sealed class AgentProvisioningSignal
|
|
{
|
|
private readonly System.Threading.Channels.Channel<bool> channel =
|
|
System.Threading.Channels.Channel.CreateBounded<bool>(
|
|
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<AgentProvisioningOptions> options,
|
|
ILogger<AgentProvisioningWorker> logger) : BackgroundService
|
|
{
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
await using (var startupScope = scopeFactory.CreateAsyncScope())
|
|
{
|
|
try
|
|
{
|
|
await startupScope.ServiceProvider
|
|
.GetRequiredService<IAgentProposalService>()
|
|
.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<IAgentProposalService>()
|
|
.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);
|
|
}
|
|
}
|
|
}
|