diff --git a/Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs b/Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs index 42d8c73f3..6d5731fef 100644 --- a/Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs +++ b/Aetheria.State.Daemon/AetheriaYmirWorldPhysics.cs @@ -5,6 +5,7 @@ namespace Aetheria.State.Daemon; public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDisposable { + private const long PersistenceRolloverJournalEntries = 64; private const string EntityPrefix = "aetheria.daemon.entity."; private const string PickupPrefix = "aetheria.daemon.pickup."; private const string PhysicalPayloadPrefix = "aetheria.physical-payload."; @@ -76,6 +77,10 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi throw new InvalidOperationException( $"Cannot persist Ymir payload world for run '{runId}' zone {zoneIndex} at frame {frameId}."); + TryRollPersistenceGeneration(state, payload: false); + if (state.PayloadSession != null) + TryRollPersistenceGeneration(state, payload: true); + return new AetheriaYmirZonePersistenceCapture( runId, zoneIndex, @@ -586,6 +591,20 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi ? value : -1; + private static void TryRollPersistenceGeneration(SessionState state, bool payload) + { + var current = payload ? state.PayloadSession : state.WorldSession; + if (current == null || current.PersistenceJournalEntryCount < PersistenceRolloverJournalEntries || + !current.TryCreateCompactedPersistenceBaseline(out var replacement)) + return; + + if (payload) + state.PayloadSession = replacement!; + else + state.WorldSession = replacement!; + current.Dispose(); + } + private static (double X, double Y) Normalize( double x, double y, @@ -598,7 +617,7 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi private sealed class SessionState(YmirSession worldSession) { - public YmirSession WorldSession { get; } = worldSession; + public YmirSession WorldSession { get; set; } = worldSession; public YmirSession? PayloadSession { get; set; } public long LastFrameId { get; set; } = -1; public int LastSimulationStepIndex { get; set; } = -1; diff --git a/docs/aetheria-current-codebase-model.md b/docs/aetheria-current-codebase-model.md index 212a87271..2c2f55b04 100644 --- a/docs/aetheria-current-codebase-model.md +++ b/docs/aetheria-current-codebase-model.md @@ -99,13 +99,21 @@ under Unity `Assets/Scripts/ServerShared/Behaviors`. an exclusive lock while selectively hydrating another record. Startup phase traces report state open, catalog, Ymir restore, and client publication costs. -The remaining restart bottleneck is inside Ymir reconstruction, not CultCache -I/O: the current resume format validates state by replaying its entire -generation journal. Indexed hydration reduced journal page loading to tens of -milliseconds, but replay remains proportional to session lifetime. Ymir needs -a bounded baseline checkpoint plus journal tail before daemon restart cost is -independent of uptime; increasing timeouts or hiding the phase is not an -acceptable substitute. +Ymir reconstruction remains full replay within a session generation, but it is +now bounded by an explicit quiescent generation rollover. When a private world +or payload journal reaches 64 commands, `AetheriaYmirWorldPhysics` asks Ymir to +create a replacement baseline before private capture. Ymir refuses while a +contact episode is active or a mutation has not reached Step; Aetheria keeps +the old replay path in that case and never infers contact safety itself. A +successful replacement is captured before the matching public frame commits, +and the prior session is disposed only after the in-memory owner swaps. + +The migration witness paid 733 ms once to restore the historical generation; +the following restart restored the compact generation in 74 ms, including +private-cache open, selected journal hydration, and native world creation. +Historical journal pages remain durable and cold. Retention/garbage collection +of obsolete generations is separate from restart authority and must not be +smuggled into filtered hydration. ## Materialized Import And Publication Ownership