Recover abandoned agent work

This commit is contained in:
Meta Vens 2026-07-11 19:40:36 +01:00
commit a88fa37c63
2 changed files with 93 additions and 5 deletions

View file

@ -22,6 +22,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
MultipleActorsUseTheSameMovementLever();
AgentClaimsAndCompletesExploreTaskThroughCommands();
SchedulerAssignsHighestPriorityCompatibleTask();
SchedulerRequeuesTaskFromDeadAgent();
SchedulerCollapsesDuplicateAssignmentMarkers();
SchedulerAssignsShortestGalaxyRoute();
AgentTraversesGalaxyRouteBeforeExecutingTask();
AttackAgentControlsOptimumRangeThroughMovementLever();
@ -604,6 +606,68 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
RequireEqual("high", agent.AssignedAgentTaskId, "agent assignment must point at the selected task");
}
private static void SchedulerRequeuesTaskFromDeadAgent()
{
var dead = Entity(0, 0, "workers");
dead.IsActive = false;
dead.AssignedAgentTaskId = "recover-work";
var replacement = Entity(1, 0, "workers");
replacement.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
var task = new AetheriaRuntimeAgentTaskCommit
{
TaskId = "recover-work",
CorporationKey = "workers",
TaskType = AetheriaRuntimeAgentTaskTypes.Explore,
ZoneIndex = 0,
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
AssignedEntityIndex = 0,
TargetPositionX = 100,
DeliveredQuantity = 3,
RequestedQuantity = 5
};
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "agent-recovery-smoke",
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [dead, replacement] }],
AgentTasks = [task]
};
AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 8);
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, task.Status,
"work abandoned by an inactive agent must return to the corporation queue and be reassigned");
RequireEqual(replacement.EntityIndex, task.AssignedEntityIndex,
"compatible active replacement must receive abandoned work");
RequireEqual(3, task.DeliveredQuantity,
"reassignment must preserve authoritative task progress");
Require(string.IsNullOrWhiteSpace(dead.AssignedAgentTaskId),
"inactive agent must not retain reservation authority");
}
private static void SchedulerCollapsesDuplicateAssignmentMarkers()
{
var first = Entity(0, 0, "workers");
first.AssignedAgentTaskId = "single-owner";
var duplicate = Entity(1, 0, "workers");
duplicate.AssignedAgentTaskId = "single-owner";
var task = AgentTask("single-owner", 1);
task.Status = AetheriaRuntimeAgentTaskStatuses.Assigned;
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "agent-single-owner-smoke",
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [first, duplicate] }],
AgentTasks = [task]
};
AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 9);
RequireEqual(1, run.Zones.SelectMany(zone => zone.Entities)
.Count(entity => string.Equals(entity.AssignedAgentTaskId, task.TaskId, StringComparison.Ordinal)),
"one task must have exactly one active carrier after reconciliation");
RequireEqual(first.EntityIndex, task.AssignedEntityIndex,
"duplicate assignment reconciliation must be deterministic");
}
private static AetheriaRuntimeAgentTaskCommit AgentTask(string id, int priority) => new()
{
TaskId = id,

View file

@ -745,17 +745,41 @@ namespace GameCult.Aetheria.State.Verse
private static void ReleaseInvalidAssignments(AetheriaRuntimeRunCheckpointCommit run)
{
var activeTaskIds = (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>())
var tasks = (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>())
.Where(task => task != null)
.ToArray();
var entities = (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.SelectMany(zone => zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
.Where(entity => entity != null)
.ToArray();
var activeTaskIds = tasks
.Where(task => task != null && string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Assigned, StringComparison.Ordinal))
.Select(task => task.TaskId)
.ToHashSet(StringComparer.Ordinal);
foreach (var entity in (run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.SelectMany(zone => zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()))
foreach (var entity in entities)
{
if (!string.IsNullOrWhiteSpace(entity.AssignedAgentTaskId) && !activeTaskIds.Contains(entity.AssignedAgentTaskId))
if (!string.IsNullOrWhiteSpace(entity.AssignedAgentTaskId) &&
(!entity.IsActive || !activeTaskIds.Contains(entity.AssignedAgentTaskId)))
entity.AssignedAgentTaskId = "";
}
foreach (var task in tasks.Where(task => string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Assigned, StringComparison.Ordinal)))
{
var carriers = entities
.Where(entity => entity.IsActive && string.Equals(entity.AssignedAgentTaskId, task.TaskId, StringComparison.Ordinal))
.OrderBy(entity => entity.EntityIndex)
.ToArray();
if (carriers.Length == 0)
{
task.Status = AetheriaRuntimeAgentTaskStatuses.Queued;
task.AssignedEntityIndex = -1;
task.AssignedFrameId = -1;
continue;
}
task.AssignedEntityIndex = carriers[0].EntityIndex;
foreach (var duplicate in carriers.Skip(1))
duplicate.AssignedAgentTaskId = "";
}
}
private static string EntityKey(AetheriaRuntimeRunCheckpointCommit run, int zoneIndex, int entityIndex) =>