Release agent controls with terminal tasks

This commit is contained in:
Meta Vens 2026-07-17 05:46:01 +01:00
commit d6562f69cb
5 changed files with 120 additions and 6 deletions

View file

@ -211,6 +211,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
SchedulerAssignsHighestPriorityCompatibleTask,
SchedulerRequeuesTaskFromDeadAgent,
SchedulerCollapsesDuplicateAssignmentMarkers,
CancellingAgentTaskReleasesSchedulerControls,
SchedulerAssignsShortestGalaxyRoute,
WormholeTraversalUsesPersistedEnterTransferExitPhases,
AgentTraversesGalaxyRouteBeforeExecutingTask,
@ -4999,6 +5000,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
first.AssignedAgentTaskId = "single-owner";
var duplicate = Entity(1, 0, "workers");
duplicate.AssignedAgentTaskId = "single-owner";
duplicate.HelmForward = 1;
duplicate.TargetEntityIndex = 7;
var task = AgentTask("single-owner", 1);
task.Status = AetheriaRuntimeAgentTaskStatuses.Assigned;
var run = new AetheriaRuntimeRunCheckpointCommit
@ -5015,6 +5018,84 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
"one task must have exactly one active carrier after reconciliation");
RequireEqual(first.EntityIndex, task.AssignedEntityIndex,
"duplicate assignment reconciliation must be deterministic");
RequireNear(0, duplicate.HelmForward, 0.000001,
"duplicate assignment reconciliation must release the losing scheduler controls");
RequireEqual(-1, duplicate.TargetEntityIndex,
"duplicate assignment reconciliation must release the losing scheduler target");
}
private static void CancellingAgentTaskReleasesSchedulerControls()
{
var agent = Entity(0, 0, "workers");
agent.AssignedAgentTaskId = "tow-station";
agent.HelmStrafe = 0.75;
agent.HelmForward = 1;
agent.TargetEntityIndex = 2;
agent.ChildEntityIndices = [2];
var station = Entity(2, 0, "workers");
station.Kind = "station";
var task = new AetheriaRuntimeAgentTaskCommit
{
TaskId = "tow-station",
CorporationKey = "workers",
TaskType = AetheriaRuntimeAgentTaskTypes.Tow,
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
AssignedEntityIndex = 0,
TargetEntityIndex = 2,
PendingQuantity = 4,
Phase = "delivery"
};
var run = new AetheriaRuntimeRunCheckpointCommit
{
RunId = "agent-cancel-smoke",
CurrentZoneIndex = 0,
CurrentEntityKey = "",
AgentTasks = [task],
Zones =
[
new AetheriaRuntimeZoneSnapshotCommit
{
ZoneIndex = 0,
Entities = [agent, Entity(1, 0, "workers"), station]
}
]
};
var cancel = AetheriaRuntimeDaemonCommandDocument.Create(
AetheriaRuntimeDaemonCommandKinds.CancelAgentTask,
"commander-smoke",
"starbridge-smoke",
12,
"");
cancel.CommandId = "cancel-tow-station";
cancel.AgentTask = new AetheriaRuntimeAgentTaskCommand { TaskId = task.TaskId };
var result = AetheriaRuntimeDaemonOperations.Execute(
run,
[cancel],
new AetheriaRuntimeDaemonOperationContext());
Require(result.AppliedCommandIds.Contains(cancel.CommandId, StringComparer.Ordinal),
"task cancellation must commit through the daemon command boundary");
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Cancelled, task.Status,
"cancelled task must become terminal immediately");
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Cancelled, task.Phase,
"cancelled task must not retain a counterfeit in-progress phase");
RequireEqual(-1, task.AssignedEntityIndex,
"cancelled task must release its assigned agent identity");
RequireEqual(0, task.PendingQuantity,
"cancelled transfer work must not retain pending quantity");
RequireEqual("", agent.AssignedAgentTaskId,
"cancelled agent must release the durable assignment marker");
RequireNear(0, agent.HelmStrafe, 0.000001,
"cancelled agent must release scheduler-owned strafe input");
RequireNear(0, agent.HelmForward, 0.000001,
"cancelled agent must release scheduler-owned forward input");
RequireEqual(-1, agent.TargetEntityIndex,
"cancelled agent must release scheduler-owned targeting");
Require(!agent.ChildEntityIndices.Contains(station.EntityIndex),
"cancelling an active tow must release the station at its authoritative world position");
RequireEqual(0, AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 13).Count,
"cancelled work must not produce another scheduler command on the next frame");
}
private static AetheriaRuntimeAgentTaskCommit AgentTask(string id, int priority) => new()

View file

@ -160,6 +160,27 @@ namespace GameCult.Aetheria.State.Verse
}
}
internal static void ReleaseControls(
AetheriaRuntimeEntitySnapshotCommit agent,
AetheriaRuntimeAgentTaskCommit? task)
{
if (agent == null)
return;
agent.AssignedAgentTaskId = "";
agent.HelmStrafe = 0;
agent.HelmForward = 0;
agent.TargetEntityIndex = -1;
if (task != null &&
string.Equals(task.TaskType, AetheriaRuntimeAgentTaskTypes.Tow, StringComparison.Ordinal) &&
task.TargetEntityIndex >= 0)
{
agent.ChildEntityIndices = (agent.ChildEntityIndices ?? Array.Empty<int>())
.Where(index => index != task.TargetEntityIndex)
.ToArray();
}
}
private static void AssignQueuedTasks(AetheriaRuntimeRunCheckpointCommit run, long frameId)
{
var tasks = (run.AgentTasks ?? Array.Empty<AetheriaRuntimeAgentTaskCommit>()).Where(task => task != null).ToArray();
@ -1171,14 +1192,14 @@ namespace GameCult.Aetheria.State.Verse
{
task.Status = AetheriaRuntimeAgentTaskStatuses.Completed;
task.CompletedFrameId = frameId;
agent.AssignedAgentTaskId = "";
ReleaseControls(agent, task);
}
private static void Fail(AetheriaRuntimeAgentTaskCommit task, AetheriaRuntimeEntitySnapshotCommit agent)
{
task.Status = AetheriaRuntimeAgentTaskStatuses.Failed;
task.PendingQuantity = 0;
agent.AssignedAgentTaskId = "";
ReleaseControls(agent, task);
}
private static string CommandId(AetheriaRuntimeAgentTaskCommit task, long frameId, string phase) =>
@ -1210,7 +1231,8 @@ namespace GameCult.Aetheria.State.Verse
{
if (!string.IsNullOrWhiteSpace(entity.AssignedAgentTaskId) &&
(!entity.IsActive || !activeTaskIds.Contains(entity.AssignedAgentTaskId)))
entity.AssignedAgentTaskId = "";
ReleaseControls(entity, tasks.FirstOrDefault(task =>
string.Equals(task.TaskId, entity.AssignedAgentTaskId, StringComparison.Ordinal)));
}
foreach (var task in tasks.Where(task => string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Assigned, StringComparison.Ordinal)))
{
@ -1227,7 +1249,7 @@ namespace GameCult.Aetheria.State.Verse
}
task.AssignedEntityIndex = carriers[0].EntityIndex;
foreach (var duplicate in carriers.Skip(1))
duplicate.AssignedAgentTaskId = "";
ReleaseControls(duplicate, task);
}
}

View file

@ -416,10 +416,12 @@ namespace GameCult.Aetheria.State.Verse
.SelectMany(zone => zone.Entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()))
{
if (string.Equals(entity.AssignedAgentTaskId, task.TaskId, StringComparison.Ordinal))
entity.AssignedAgentTaskId = "";
AetheriaRuntimeAgentScheduler.ReleaseControls(entity, task);
}
task.Status = AetheriaRuntimeAgentTaskStatuses.Cancelled;
task.AssignedEntityIndex = -1;
task.PendingQuantity = 0;
task.Phase = AetheriaRuntimeAgentTaskStatuses.Cancelled;
return true;
}

View file

@ -1133,6 +1133,15 @@ The task's weapon-group field is now an observed last selection, not an attack
policy owner. Daemon smoke proves close/open strafe, DPS group selection,
projectile lead, hardpoint gating, progressive lock, and target destruction.
Task assignment is the only owner of autonomous helm and target controls for an
uncontrolled agent. Cancel, complete, failure, stale-assignment repair, and
duplicate-carrier repair all use `AetheriaRuntimeAgentScheduler.ReleaseControls`.
That transition clears the durable assignment marker, both helm axes, and the
scheduler target; a tow task also releases its target from the agent's child
list at the already-authoritative world position. Task status and phase are
terminal facts after cancellation. They are not a repair signal that permits
the previous controller state to survive for another tick.
### Authoritative Flight Actuator Contract
- Owner: Aetheria daemon owns persistent local helm axes, desired look

View file

@ -45,7 +45,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; `Switch`, one-shot `Trigger`, normalized `Cooldown`, adjustable high/low-pass `Thermotoggle`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; each row records whether the chain reached it. `Thruster`, rare `AetherDrive`, and `Radiator` now pause their groups at the specialized owner, report the exact fossil success result, and resume common tails at most once, so idle or unfunded actuators cannot consume wear; radiator emission remains its independent always-update path. Cooldown projects generic progress and thermostat target is daemon-commanded state seeded from authored data. Equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | remaining specialized return-value composition and canonical consumable-content proof |
| Sensors/visibility | installed sensor sensitivity and angular curve, `Visibility`, `Reflector`, decaying signature sources, stellar light, ping state, and observer information thresholds | daemon | accumulated contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | every operational installed Sensor executes the fossil visibility/sensitivity/angle/distance information equation and decay; visibility is thresholded by the provider setting. Actor-scoped ping owns energy, cooldown, expansion radius, reconnect-stable crossed-target state, one-time information boost, and transient signature. Operational `Visibility` equipment publishes evaluated constant signature; committed weapons publish authored signature including charged-shot multiplication; transient weapon and ping sources decay by the fossil exponential rule. Reflectors multiply evaluated cross-section by the exponent-8 stellar `PowerPulse` after thermal settlement. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer invents perfect information or overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance | active consumable visibility/reflector effects, complete source cardinality, classification timelines, and canonical catalog station-array data |
| AI/tasks | corporation task scheduler plus agent state machines and installed orbital `TurretController` | daemon | commander task board, corporation survey ledger, controller aim/state, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Ship combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Chain-admitted orbital controllers independently acquire the first visible hostile ship, publish predictive look direction, and trigger only exact aligned in-range weapon behaviors through the shared combat lifecycle; the canonical preceding `EnergyDraw` now blocks acquisition, aim, and fire when power is unavailable, and non-player identity grants no implicit group-zero fire. Cross-zone task and home routes survive the full daemon-owned wormhole timeline; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing | cancellation, reconnect, multi-client command transfer, and model-local barrel-direction metadata |
| AI/tasks | corporation task scheduler plus agent state machines and installed orbital `TurretController` | daemon | commander task board, corporation survey ledger, controller aim/state, and shared semantic controls | Eve commander/pilot clients | point exploration, loadout-driven attack, haul, mining/offload, resource surveying, physical station towing, persistent orbit patrol, route-aware assignment, and autonomous return home are wired through shared commands. Ship combat samples the fossil DPS/range policy, selects live weapon groups, leads projectiles, strafes relative to target attitude, and gates fire on catalog placement rotation. Chain-admitted orbital controllers independently acquire the first visible hostile ship, publish predictive look direction, and trigger only exact aligned in-range weapon behaviors through the shared combat lifecycle; the canonical preceding `EnergyDraw` now blocks acquisition, aim, and fire when power is unavailable, and non-player identity grants no implicit group-zero fire. Cross-zone task and home routes survive the full daemon-owned wormhole timeline; attack admission rejects missing/friendly targets and unavailable weapon groups before durable queueing. Cancel, complete, fail, stale-assignment repair, and duplicate-carrier repair share one scheduler-owned release primitive that zeros persisted helm input, clears the scheduler target, releases towing parentage, and prevents a terminal task from emitting another command | reconnect, multi-client command transfer, and model-local barrel-direction metadata |
| Consumables | cargo-backed timed behavior containers | daemon | dynamic actions, active effects, and lifecycle facts | action bar/schematic | daemon atomically resolves typed metadata and concrete cargo, preserves consumed-instance quality, enforces authored stacking, evaluates effectiveness/quality, and executes ordered `EnergyDraw`, fossil-compatible no-op `Heat`, `ItemUsage`, and stateful `Cooldown` payloads after energy tick initialization; every effect and payload state has stable authored identity; cooldowns update before the ordered chain, require state strictly below zero, reset to one on success, and continue aging behind earlier failures; false and unsupported payloads stop later execution with a typed fact; timers retain the fossil's exact-zero final tick; Unity reconciles presentation by effect identity and cannot restore active behavior state by list index | catalog revision pinning, stat modifiers, remaining behavior families, and reconnect presentation proof |
## Ownership Invariants