using Microsoft.EntityFrameworkCore; using Nexus.Api.Data; using Nexus.Api.Models; using System.Text.Json; namespace Nexus.Api.Repositories; public sealed class OpenClawRunRepository(NexusDbContext db) : IOpenClawRunRepository { public async Task> GetAsync( OpenClawRunQuery query, CancellationToken cancellationToken = default) { var runs = db.OpenClawRuns.AsNoTracking().AsQueryable(); if (!string.IsNullOrWhiteSpace(query.Status)) runs = runs.Where(run => run.Status == query.Status); if (query.TaskId.HasValue) runs = runs.Where(run => run.TaskId == query.TaskId); if (query.ProjectId.HasValue) runs = runs.Where(run => run.ProjectId == query.ProjectId); if (!string.IsNullOrWhiteSpace(query.SessionKey)) runs = runs.Where(run => run.SessionKey == query.SessionKey); if (OpenClawRunCursorCodec.TryDecode(query.Cursor, out var cursor)) { var createdBefore = cursor.CreatedAt; if (cursor.Id.HasValue) { var idBefore = cursor.Id.Value; runs = runs.Where(run => run.CreatedAt < createdBefore || (run.CreatedAt == createdBefore && run.Id.CompareTo(idBefore) < 0)); } else { runs = runs.Where(run => run.CreatedAt < createdBefore); } } return await runs .OrderByDescending(run => run.CreatedAt) .ThenByDescending(run => run.Id) .Take(Math.Clamp(query.Limit, 1, 201)) .ToListAsync(cancellationToken); } public Task GetByIdAsync( Guid id, bool tracking = false, CancellationToken cancellationToken = default) { var query = tracking ? db.OpenClawRuns.AsQueryable() : db.OpenClawRuns.AsNoTracking(); return query.FirstOrDefaultAsync(run => run.Id == id, cancellationToken); } public Task GetByStartIdempotencyKeyAsync( string idempotencyKey, CancellationToken cancellationToken = default) => db.OpenClawRuns .AsNoTracking() .FirstOrDefaultAsync(run => run.StartIdempotencyKey == idempotencyKey, cancellationToken); public Task GetByOpenClawRunIdAsync( string openClawRunId, CancellationToken cancellationToken = default) => db.OpenClawRuns .FirstOrDefaultAsync(run => run.OpenClawRunId == openClawRunId, cancellationToken); public async Task> GetHistoryAsync( Guid runId, CancellationToken cancellationToken = default) => await db.OpenClawRunHistory .AsNoTracking() .Where(item => item.RunId == runId) .OrderBy(item => item.OccurredAt) .ThenBy(item => item.Id) .ToListAsync(cancellationToken); public Task GetInvocationAsync( Guid runId, string action, string idempotencyKey, CancellationToken cancellationToken = default) => db.OpenClawRunHistory .AsNoTracking() .FirstOrDefaultAsync( item => item.RunId == runId && item.Action == action && item.IdempotencyKey == idempotencyKey, cancellationToken); public Task HasGatewayEventAsync( string gatewayEventId, CancellationToken cancellationToken = default) => db.OpenClawRunHistory .AsNoTracking() .AnyAsync(item => item.GatewayEventId == gatewayEventId, cancellationToken); public Task TaskExistsAsync(Guid taskId, CancellationToken cancellationToken = default) => db.Tasks.AsNoTracking().AnyAsync(task => task.Id == taskId, cancellationToken); public Task ProjectExistsAsync(Guid projectId, CancellationToken cancellationToken = default) => db.Projects.AsNoTracking().AnyAsync(project => project.Id == projectId, cancellationToken); public async Task> GetActiveSubscriptionsAsync( CancellationToken cancellationToken = default) { var subscriptions = await db.OpenClawRuns .AsNoTracking() .Where(run => run.Status == OpenClawRunStates.Dispatching || run.Status == OpenClawRunStates.Running || run.Status == OpenClawRunStates.Stopping) .Select(run => new { run.SessionKey, run.AgentId }) .Distinct() .ToListAsync(cancellationToken); return subscriptions .Select(item => new OpenClawRunSubscription(item.SessionKey, item.AgentId)) .ToList(); } public async Task AddAsync( OpenClawRun run, OpenClawRunHistory history, CancellationToken cancellationToken = default) { db.OpenClawRuns.Add(run); db.OpenClawRunHistory.Add(history); db.OutboxEvents.Add(CreateRunEvent("run.created", run)); await db.SaveChangesAsync(cancellationToken); } public async Task AddRetryAsync( OpenClawRun source, OpenClawRun retry, OpenClawRunHistory sourceHistory, OpenClawRunHistory retryHistory, CancellationToken cancellationToken = default) { source.UpdatedAt = DateTimeOffset.UtcNow; source.Revision++; db.OpenClawRuns.Update(source); db.OpenClawRuns.Add(retry); db.OpenClawRunHistory.AddRange(sourceHistory, retryHistory); db.OutboxEvents.Add(CreateRunEvent("run.updated", source)); db.OutboxEvents.Add(CreateRunEvent( "run.created", retry, source.Id)); await db.SaveChangesAsync(cancellationToken); } public async Task UpdateAsync( OpenClawRun run, OpenClawRunHistory history, CancellationToken cancellationToken = default) { run.UpdatedAt = DateTimeOffset.UtcNow; run.Revision++; db.OpenClawRuns.Update(run); db.OpenClawRunHistory.Add(history); db.OutboxEvents.Add(CreateRunEvent("run.updated", run)); await db.SaveChangesAsync(cancellationToken); } public async Task UpdateProjectionAsync( OpenClawRun run, OpenClawRunHistory? history = null, CancellationToken cancellationToken = default) { run.UpdatedAt = DateTimeOffset.UtcNow; run.Revision++; db.OpenClawRuns.Update(run); if (history is not null) db.OpenClawRunHistory.Add(history); db.OutboxEvents.Add(CreateRunEvent("run.updated", run)); await db.SaveChangesAsync(cancellationToken); } private static OutboxEvent CreateRunEvent( string type, OpenClawRun run, Guid? sourceRunId = null) => new() { Type = type, AggregateType = "run", AggregateId = run.Id.ToString(), AggregateRevision = run.Revision, PayloadJson = JsonSerializer.Serialize(new { status = run.Status, taskId = run.TaskId, projectId = run.ProjectId, agentId = run.AgentId, sourceRunId }) }; }