From 02c004897ca1babd19575c44cb34a70d2de657bf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 20 Jul 2026 02:07:48 +0100 Subject: [PATCH] Serve committed Eve state before restart tick --- Aetheria.State.Daemon/Program.cs | 93 ++++++++++++++++------ Aetheria.State/AetheriaDocumentRegistry.cs | 1 + Aetheria.State/AetheriaStateNode.cs | 8 ++ docs/aetheria-current-codebase-model.md | 27 +++++++ 4 files changed, 104 insertions(+), 25 deletions(-) diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 454bf1383..0e4da6ce7 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -116,36 +116,63 @@ var nextApiPublicationUtc = DateTimeOffset.UtcNow; var ingressState = new AetheriaDaemonIngressState(); var hotState = new AetheriaHotEntityPublicationState(); var reactiveSurfaceState = new AetheriaReactiveSurfacePublicationState(); -var firstTick = await TickAsync(node, options, unityBundles, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false); -TraceStartup("first-tick"); +var publishRestoredFrame = !options.Once && + latestFrame?.Run != null && + HasPlayableRun(latestFrame.Run) && + HasPersistedClientBootstrap(node, latestFrame); +AetheriaRuntimeDaemonTickResult? initialPublication = null; +AetheriaPreparedPublication? initialPrepared = null; +Task publicationTask; +if (publishRestoredFrame) +{ + await RefreshControlPlaneInputsAsync(node, ingressState).ConfigureAwait(false); + TraceStartup("restored-client-state"); +} +else +{ + var firstTick = await TickAsync( + node, options, unityBundles, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false); + TraceStartup("first-tick"); + latestFrame = firstTick.Frame; + initialPrepared = PreparePublication( + node.StatePath, + options, + firstTick, + ingressState, + physicsPersistence); + initialPublication = initialPrepared.Publication; +} ThrowIfClientHostFaulted(cultMeshClientHost); -latestFrame = firstTick.Frame; -var firstPublication = PreparePublication( - node.StatePath, - options, - firstTick, - ingressState, - physicsPersistence); -await PublishClientGameplayDocumentsAsync( - node, - options, - unityBundles, - firstPublication.Publication, - firstPublication.Catalog, - reactiveSurfaceState, - publishTopology: true).ConfigureAwait(false); +if (!publishRestoredFrame) +{ + await PublishClientGameplayDocumentsAsync( + node, + options, + unityBundles, + initialPublication!, + ingressState.Catalog ?? throw new InvalidOperationException("Aetheria runtime catalog was not initialized."), + reactiveSurfaceState, + publishTopology: true).ConfigureAwait(false); +} TraceStartup("client-bootstrap"); await PublishHotEntityStateAsync( - node, soaPublisher, hotState, firstTick.Frame, ingressState.Catalog, cultMeshClientHost).ConfigureAwait(false); + node, soaPublisher, hotState, latestFrame!, ingressState.Catalog, cultMeshClientHost).ConfigureAwait(false); TraceStartup("hot-entity-state"); -Task publicationTask = Task.Run(() => PersistPreparedDocumentsAsync( - node, - options, - physicsPersistence, - firstPublication, - initialTopology: true)); +if (publishRestoredFrame) +{ + publicationTask = Task.CompletedTask; +} +else +{ + publicationTask = Task.Run(() => PersistPreparedDocumentsAsync( + node, + options, + physicsPersistence, + initialPrepared!, + initialTopology: true)); +} nextApiPublicationUtc = DateTimeOffset.UtcNow.Add(options.ApiPublicationInterval); -Console.WriteLine($"Aetheria Verse daemon published frame {firstTick.Frame.FrameId}."); +Console.WriteLine($"Aetheria Verse daemon published frame {latestFrame!.FrameId}."); Console.WriteLine($"Aetheria daemon playable-world-ready in {startup.Elapsed.TotalMilliseconds:0.###}ms."); if (options.Once) @@ -466,6 +493,22 @@ static AetheriaPreparedPublication PreparePublication( ingressState.Catalog ?? throw new InvalidOperationException("Aetheria runtime catalog was not initialized.")); } +static bool HasPersistedClientBootstrap( + AetheriaStateNode node, + AetheriaRuntimeDaemonFrameDocument frame) +{ + var input = node.Cache.Get(AetheriaRuntimeVerseRecordKeys.PilotInputCapability); + var game = node.Cache.Get(AetheriaRuntimeVerseRecordKeys.DaemonGameSurface); + var reactive = node.Cache.Get(AetheriaRuntimeVerseRecordKeys.DaemonGameReactiveSurface); + var map = node.Cache.Get(AetheriaRuntimeVerseRecordKeys.MapMenuSurface); + return node.Cache.Get(AetheriaRuntimeVerseRecordKeys.EveProviderAdvertisement) != null && + node.Cache.Get(AetheriaRuntimeVerseRecordKeys.EveAssetCatalog) != null && + input?.Version == frame.FrameId && + game?.Version == frame.FrameId && + reactive?.Version == frame.FrameId && + map?.Version == frame.FrameId; +} + static async Task PersistPreparedDocumentsAsync( AetheriaStateNode node, AetheriaDaemonHostOptions options, diff --git a/Aetheria.State/AetheriaDocumentRegistry.cs b/Aetheria.State/AetheriaDocumentRegistry.cs index 38937d1e1..5ac01d9a2 100644 --- a/Aetheria.State/AetheriaDocumentRegistry.cs +++ b/Aetheria.State/AetheriaDocumentRegistry.cs @@ -46,6 +46,7 @@ public static class AetheriaDocumentRegistry typeof(AetheriaRuntimeDaemonProviderAdvertisementDocument), typeof(AetheriaRuntimeDaemonHealthDocument), typeof(AetheriaRuntimeDaemonCommandBoundaryDocument), + typeof(AetheriaRuntimeAssetManifestDocument), typeof(AetheriaRuntimeDaemonFrameDocument), typeof(AetheriaRuntimeGameViewportDocument), typeof(AetheriaRuntimeObjectsViewportDocument), diff --git a/Aetheria.State/AetheriaStateNode.cs b/Aetheria.State/AetheriaStateNode.cs index e6dd99d31..c98d72a9e 100644 --- a/Aetheria.State/AetheriaStateNode.cs +++ b/Aetheria.State/AetheriaStateNode.cs @@ -127,6 +127,14 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable MainMenuStateKey.ToString(), EveCommandAcceptanceStatusKey.ToString(), AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(), + AetheriaRuntimeVerseRecordKeys.DaemonProviderAdvertisement.ToString(), + AetheriaRuntimeVerseRecordKeys.EveProviderAdvertisement.ToString(), + AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest.ToString(), + AetheriaRuntimeVerseRecordKeys.EveAssetCatalog.ToString(), + AetheriaRuntimeVerseRecordKeys.PilotInputCapability.ToString(), + AetheriaRuntimeVerseRecordKeys.DaemonGameSurface.ToString(), + AetheriaRuntimeVerseRecordKeys.DaemonGameReactiveSurface.ToString(), + AetheriaRuntimeVerseRecordKeys.MapMenuSurface.ToString(), AetheriaRuntimeVerseRecordKeys.VerseAuthorityPolicy.ToString(), AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest.ToString(), AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest.ToString() diff --git a/docs/aetheria-current-codebase-model.md b/docs/aetheria-current-codebase-model.md index 2c2f55b04..1ccedbd9a 100644 --- a/docs/aetheria-current-codebase-model.md +++ b/docs/aetheria-current-codebase-model.md @@ -115,6 +115,33 @@ 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. +### Restored-frame client readiness + +- **Owner:** the latest hard-flushed daemon frame owns restart world truth. Its + frame-versioned Eve input, pilot topology, reactive fragment, and map surface + are durable projections of that same boundary. +- **Inputs:** exact persisted Eve bootstrap record keys plus the latest frame; + the daemon boot filter hydrates those records without loading unrelated menu + surfaces or history. +- **Outputs:** on an ordinary restored daemon run, CultMesh can advertise the + committed Eve surfaces and publish a fresh hot SoA body before frame N+1 is + simulated. New stores and explicit `--once` runs retain the initialization + tick path. +- **Derived state:** provider advertisement and asset catalog are stable + bootstrap projections. Input and Eve surfaces qualify for the fast path only + when every record version exactly equals the durable frame id; missing or + stale records force a normal rebuild. +- **Forbidden writers:** startup must not advance simulation merely to make an + already committed frame presentable. It also must not pair a current frame + with stale client projections or let existence alone prove consistency. +- **Cut line:** restored readiness no longer calls the simulation tick, + `BuildPublications`, or the surface lowerers. Ordinary ticking resumes after + readiness and owns frame N+1. +- **Verification layer:** the restored artifact reached client bootstrap in + under one millisecond and playable readiness in 1.41–1.55 seconds, down from + 2.19 seconds after Ymir compaction alone. The explicit `--once` witness still + executed and committed one new frame. + ## Materialized Import And Publication Ownership ### Catalog import