feat: ship agent-first mission control v0.2.57
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user