From d6562f69cb82f3ecba4f85463356af464f8da262 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 17 Jul 2026 05:46:01 +0100 Subject: [PATCH] Release agent controls with terminal tasks --- Aetheria.State.Daemon.Smoke/Program.cs | 81 +++++++++++++++++++ .../Runtime/AetheriaRuntimeAgentTasks.cs | 30 ++++++- .../AetheriaRuntimeDaemonOperations.cs | 4 +- docs/aetheria-perfect-machine-map.md | 9 +++ docs/original-game-migration-matrix.md | 2 +- 5 files changed, 120 insertions(+), 6 deletions(-) diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 0220fcccb..783ea9f35 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -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() diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs index 3783476e8..2a730cc1b 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeAgentTasks.cs @@ -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()) + .Where(index => index != task.TargetEntityIndex) + .ToArray(); + } + } + private static void AssignQueuedTasks(AetheriaRuntimeRunCheckpointCommit run, long frameId) { var tasks = (run.AgentTasks ?? Array.Empty()).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); } } diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs index 1fe13b7d0..d1f87b2b5 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonOperations.cs @@ -416,10 +416,12 @@ namespace GameCult.Aetheria.State.Verse .SelectMany(zone => zone.Entities ?? Array.Empty())) { 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; } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 9488a359c..6632122b6 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -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 diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 4ca91b672..855f6b1f6 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -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