feat: ship agent-first mission control v0.2.57
CI - Build & Test / Backend (.NET) (push) Successful in 42s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 2m46s
CI - Build & Test / Security Check (push) Successful in 3s
CI - Build & Test / Deploy Nexus (push) Successful in 56s

This commit is contained in:
AzuTear
2026-07-31 22:39:47 +02:00
parent 3bc7622977
commit f5552218bc
535 changed files with 95242 additions and 8791 deletions
@@ -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);
}
}
}