From 5c19d515f0ce252f70b02c3965ffdbb171e5d0fb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 14 Jun 2026 12:31:20 +0100 Subject: [PATCH] Publish runtime sessions as typed state --- .voidbot/state/aetheria.cc | Bin 85721 -> 85751 bytes Aetheria.State.ApplyPending/Program.cs | 19 ++++++++- Aetheria.State.Smoke/Program.cs | 31 +++++++++++++-- .../AetheriaOperationsSurfaceProjector.cs | 36 ++++++++++++++---- .../AetheriaProviderAdvertisementProjector.cs | 1 + Aetheria.State/AetheriaStateNode.cs | 11 ++++++ .../Documents/AetheriaRuntimeSession.cs | 3 ++ Aetheria.State/README.md | 32 ++++++++++------ Economy.Server/Program.cs | 32 ++++++++++++++-- docs/aetheria-perfect-machine-map.md | 8 +++- 10 files changed, 145 insertions(+), 28 deletions(-) diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index 5cb32befba9d7517f60eeac570a754dca6de45a8..ca672f540e5b8ff2f8c47033bd62a66e8bdc96ec 100644 GIT binary patch delta 1953 zcmZuy&rcmi9M872p{7yN6wtPTFW4GMcnc5SD+OY;0{)5>0wyMegg0+@-rLdLna#{B zgkA_~;)RQ5YxaZ(8-Jvl)Eh|;wI0+fSG?GNpfOzZ(x~5=-ADQ1f$-k<&3u1+etc(l z>(t*{r+)nA#B-y=6T_qKXnB0`bEh-?<-}$yGBRc(;^eJXWOUd@+S|Z+aC@L_d!Wtv z=iK%Oea<_5*{^4v&WkePnnkpvs8USulF~%ux0rF7?Y-(Ot*aYU2UIdLpPyXF4+2vV zvK0z?Ev7!$YlA>|@ex!cKA{StL}}nc!>VMOf?DI4OfwIpSXIEd7T^&sIP*vdZMHnyXMFMQ3g?#Bwo&yHc*fR1)eb;stcbe7Z?h z0KV{&h;pr5nDGP`(VDwJ6M3e*%*8mxa za^MkjLkfbRNyIo(jyWH_dttrieyb@rO4!|*r%lRjhd9`AmOxTZNMD(sz>|mvg&=8C zc>k>h@Pb5gD~ARN6~!}}gaXqDR1vb*I3NX06^2wmB%QaoV6HG%6vRXaE=(I#Z6v|F z$P^7}M73PA74|h&$A%&bKB19OaxT$20k-u_WQu^-=(RLBKHT_OFX&O)ZwP`rL2 z-Vzr+g;+?f1|gPWh54uv;-gYDQ&^enLCe?y0`b_#)H7t}E@*^_3Wg-bBiQ-6aRUyI z8Ay}5fL>}jL}$a&gOU!;%6OX;J9_R~35sWbB8zyLO9Rzgdx`?h%|Ow&lB z(wASDc>Rq9voOFoqosN*j0dnDZw?h!8d6%sxKo;_=tIfV;kirc&g_}=hVIXLUvWCB z)!ApRIOYg5^pOlWr1aCCu5$LY>U2#`=Ka|9)AEKj1p2|M#qeo5%qK?UO2rf+=+Sg& zZ0^0un_R&(-)+l8JJ70B&Y5jp3rC=t5?Va@B-+Ho*9;ee;GXEg^ve)VAe=vuf zA)%yc7KJ`Qtb6;eq>bIhw7l7!?rmPezl-U^C;eC5BUkoWEj+TybO7aY)%sf%Y36L2 z4VRr;6nrc0eukclW)~Cb3b<1LfC93XU;jlk8|dq zTf_Hm4d3}e(IbvH#K{8k319qGVsBn>?Hnsa6A@+H}Z(-85=P&M9IgyXNLCA z43URVoh~uZ4Uj%7g}^$*f((20@#0&yPEvFcZY!1!gh0NVzn?QG@uR$uR z3^##)157>D!C(DYCM^}A6fNHaSyK2(Sr`Vs%e0WHT$8@HiIjNImx(H7xa`t{q(NQC zv@ps*UB(5+EJ4P&kd(O{7ea0RTND&K9I5RAuADC5F48622GR>n=5uUE;;(WGk$EU* z*>v14O9+M3RPdm^Kz)sZVdGqA3QhdWl&Qdj9w~FCN!=$a-22JY`US5s@{W_>dr;t}gwWhC3-LmH^ zSF}L39lFu*6~!Os8lqSYOEF`|h46phD}dq66148^~(8_w0&wWEBYn>?vR#MktD&`QHTD1Lzrx)S{lP+Cc|c zg1Zi~^!FIZsBMXL=qo8RId8q3fN#OU+o(PcIL^i+qk*0e{2E9!SkR`}R#UWIDi}tc zxE9?uQX}U=7h0)E)|m?`d2pH>BUj=F*QerN*^}|t+lPnD_{MAZPbPnkkVBPxbL0jz zU0V~UYb>2p)(!*;wX*Ssitk^aH>sZJPD!3{ac09#xnZR@o!NR`wt;LuK(zxAf=TG? z-j-0mSB~#qd^)L&l9Bj_-^b%`W{+%LTFur2O}7Dp;|h1H%9cql`FVQuFcw2n6wRj8 zSUuk?_}%#y%*x>|tV3PJY4OYP-pyAJFKTFpB`JhHACvRPJMXSCrNCP#$j>fc7=#fUDfaycRTi|LF$Z5pW*xVKQG?)qQUyk-QaCYP1FCBmQ>1gK?_jQ}* z8+JQ_2s7rg^}j1wa3f~b^nJD!@9sUjwsuk2X_I!s z%ITV?s@<%KKxOL@ooQc}Uiq^?baB3VGZK?ej*=6@XDjnFv&m0K$*ITf*ZE4tsZ=V- zwIwpK_+5svWHH{!9F;$`>U+yt@!qQ2(1NdA+<54a#{S6@ 0 ? args[0] : Directory.GetCurrentDirectory(); -var statePath = AetheriaStatePaths.ResolveDefaultStatePath(root); +var stateDirectory = Path.Combine(Path.GetTempPath(), "aetheria-state-smoke", Guid.NewGuid().ToString("N")); +var statePath = Path.Combine(stateDirectory, "aetheria-world.cc"); var now = DateTimeOffset.UtcNow.ToString("O"); var itemLegacyId = "smoke:aether-drive"; var factionLegacyId = "smoke:faction"; @@ -106,6 +107,17 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s await node.PutRuntimeCommitDrainStatusAsync(drainStatus); await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(drainStatus)); await node.PutProviderAdvertisementAsync(AetheriaProviderAdvertisementProjector.Build(statePath, now)); + await node.PutRuntimeSessionAsync(new AetheriaRuntimeSession + { + RuntimeId = "smoke-runtime", + Role = "state-smoke", + StartedAtUtc = now, + LastSeenAtUtc = now, + Status = "running" + }); + await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build( + drainStatus, + runtimeSession: await node.GetRuntimeSessionAsync("smoke-runtime"))); AetheriaRuntimeEveCommandLog.QueueCommand( statePath, @@ -142,7 +154,10 @@ await using (var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-s Status = eveCommandReport.RejectedCommands > 0 ? "rejected" : "ok" }; await node.PutEveCommandDrainStatusAsync(eveCommandStatus); - await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(drainStatus, eveCommandStatus)); + await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build( + drainStatus, + eveCommandStatus, + await node.GetRuntimeSessionAsync("smoke-runtime"))); await node.PutLoadoutTemplateAsync(loadoutKey, new AetheriaLoadoutTemplate { @@ -298,6 +313,7 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether var eveCommandStatus = await reopened.GetEveCommandDrainStatusAsync(); var operationsSurface = await reopened.GetOperationsSurfaceAsync(); var advertisement = await reopened.GetProviderAdvertisementAsync(); + var runtimeSession = await reopened.GetRuntimeSessionAsync("smoke-runtime"); var playerSettings = await reopened.GetPlayerSettingsAsync(); var loadout = await reopened.GetLoadoutTemplateAsync(loadoutKey); var runState = await reopened.GetRunStateAsync(runKey); @@ -342,7 +358,8 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether eveCommandStatus.AppliedCatalogRefreshes != 1 || !eveCommandStatus.LastRejectedReason.Contains("not advertised", StringComparison.Ordinal) || operationsSurface?.Surface.Id != AetheriaOperationsSurfaceProjector.SurfaceId || - !operationsSurface.Surface.Root.Children.Any(child => child.Id == "aetheria.operations.eveCommandDrain")) + !operationsSurface.Surface.Root.Children.Any(child => child.Id == "aetheria.operations.eveCommandDrain") || + !operationsSurface.Surface.Root.Children.Any(child => child.Id == "aetheria.operations.runtimeSession")) { throw new InvalidOperationException("Runtime commit/Eve command drain status or operations surface did not survive flush/reopen."); } @@ -351,11 +368,19 @@ await using (var reopened = await AetheriaStateNode.OpenAsync(statePath, "aether advertisement.Surfaces.Length < 2 || !advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaCatalogSurfaceProjector.SurfaceId) || !advertisement.Surfaces.Any(surface => surface.SurfaceId == AetheriaOperationsSurfaceProjector.SurfaceId) || + !advertisement.Schemas.Contains("aetheria.runtime_session.v1") || !advertisement.Schemas.Contains(AetheriaEveCommandBridge.CommandSchema)) { throw new InvalidOperationException("Aetheria Eve provider advertisement did not survive flush/reopen."); } + if (runtimeSession?.RuntimeId != "smoke-runtime" || + runtimeSession.Role != "state-smoke" || + runtimeSession.Status != "running") + { + throw new InvalidOperationException("Runtime session document did not survive flush/reopen."); + } + if (playerSettings?.ActiveRunKey != runKey.ToString() || playerSettings.PlayerName != "Smoke Pilot" || !playerSettings.TutorialPassed || diff --git a/Aetheria.State/AetheriaOperationsSurfaceProjector.cs b/Aetheria.State/AetheriaOperationsSurfaceProjector.cs index 96498b436..ed08ae252 100644 --- a/Aetheria.State/AetheriaOperationsSurfaceProjector.cs +++ b/Aetheria.State/AetheriaOperationsSurfaceProjector.cs @@ -10,9 +10,13 @@ public static class AetheriaOperationsSurfaceProjector public static EveSurfaceState Build( AetheriaRuntimeCommitDrainStatus drainStatus, AetheriaEveCommandDrainStatus? eveCommandStatus = null, + AetheriaRuntimeSession? runtimeSession = null, long version = 1) { - var updatedAtUtc = LatestTimestamp(drainStatus.LastPollAtUtc, eveCommandStatus?.LastPollAtUtc); + var updatedAtUtc = LatestTimestamp( + drainStatus.LastPollAtUtc, + eveCommandStatus?.LastPollAtUtc, + runtimeSession?.LastSeenAtUtc); return new EveSurfaceState { ProviderId = "aetheria", @@ -62,7 +66,18 @@ public static class AetheriaOperationsSurfaceProjector ("lastAccepted", eveCommandStatus?.LastAcceptedAtUtc ?? ""), ("lastRejected", eveCommandStatus?.LastRejectedCommand ?? ""), ("rejectedReason", eveCommandStatus?.LastRejectedReason ?? ""), - ("error", eveCommandStatus?.LastError ?? "")))) + ("error", eveCommandStatus?.LastError ?? ""))), + Node( + "aetheria.operations.runtimeSession", + "card", + [("title", "Runtime Session")], + Metric("runtimeSession.status", "Status", runtimeSession?.Status ?? "missing"), + Metric("runtimeSession.role", "Role", runtimeSession?.Role ?? ""), + Row( + "runtimeSession.last", + ("runtime", runtimeSession?.RuntimeId ?? ""), + ("started", runtimeSession?.StartedAtUtc ?? ""), + ("lastSeen", runtimeSession?.LastSeenAtUtc ?? "")))) }, Commands = [ @@ -76,14 +91,19 @@ public static class AetheriaOperationsSurfaceProjector }; } - private static string LatestTimestamp(string first, string? second) + private static string LatestTimestamp(params string?[] timestamps) { - if (string.IsNullOrWhiteSpace(second)) - return first; - if (string.IsNullOrWhiteSpace(first)) - return second; + var latest = ""; + foreach (var timestamp in timestamps) + { + if (!string.IsNullOrWhiteSpace(timestamp) && + (string.IsNullOrWhiteSpace(latest) || string.CompareOrdinal(timestamp, latest) > 0)) + { + latest = timestamp; + } + } - return string.CompareOrdinal(second, first) > 0 ? second : first; + return latest; } private static EveSurfaceComponent Metric(string id, string label, string value) diff --git a/Aetheria.State/AetheriaProviderAdvertisementProjector.cs b/Aetheria.State/AetheriaProviderAdvertisementProjector.cs index 9507e0197..38f43df9c 100644 --- a/Aetheria.State/AetheriaProviderAdvertisementProjector.cs +++ b/Aetheria.State/AetheriaProviderAdvertisementProjector.cs @@ -38,6 +38,7 @@ public static class AetheriaProviderAdvertisementProjector "aetheria.run_state.v1", "aetheria.zone_state.v1", "aetheria.entity_snapshot.v1", + "aetheria.runtime_session.v1", "aetheria.runtime_commit_drain_status.v1", "aetheria.eve_command_drain_status.v1", "gamecult.eve.surface.v1", diff --git a/Aetheria.State/AetheriaStateNode.cs b/Aetheria.State/AetheriaStateNode.cs index 860baff18..b5d23e903 100644 --- a/Aetheria.State/AetheriaStateNode.cs +++ b/Aetheria.State/AetheriaStateNode.cs @@ -195,6 +195,17 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable new CultRecordKey(AetheriaProviderAdvertisementProjector.AdvertisementKey)); } + public Task> PutRuntimeSessionAsync(AetheriaRuntimeSession session) + { + return Database.PutAsync(new CultRecordKey($"runtime:{session.RuntimeId}:aetheria.runtime_session.v1"), session); + } + + public Task GetRuntimeSessionAsync(string runtimeId) + { + return Database.GetAsync( + new CultRecordKey($"runtime:{runtimeId}:aetheria.runtime_session.v1")); + } + public Task> PutPlayerSettingsAsync(AetheriaPlayerSettings settings) { return Database.PutAsync(new CultRecordKey("global:aetheria.player_settings.v1"), settings); diff --git a/Aetheria.State/Documents/AetheriaRuntimeSession.cs b/Aetheria.State/Documents/AetheriaRuntimeSession.cs index 8ad1a83f7..c01d51fde 100644 --- a/Aetheria.State/Documents/AetheriaRuntimeSession.cs +++ b/Aetheria.State/Documents/AetheriaRuntimeSession.cs @@ -20,4 +20,7 @@ public sealed class AetheriaRuntimeSession [Key(3)] public string LastSeenAtUtc { get; set; } = ""; + + [Key(4)] + public string Status { get; set; } = "running"; } diff --git a/Aetheria.State/README.md b/Aetheria.State/README.md index 5439051da..f785f6193 100644 --- a/Aetheria.State/README.md +++ b/Aetheria.State/README.md @@ -13,11 +13,11 @@ are migration inputs only. The typed runtime state model is split into player settings, loadout templates, run state, zone state, entity snapshots, item slots, weapon groups, action-bar -bindings, and stat grids. Runtime checkpoints include current action-bar -bindings as stable typed targets: input control path, binding kind, item -definition reference, equipment index, behavior index, or weapon-group index. -They also include current faction relationship rows as typed corporation -references plus relationship tokens and numeric standings. +bindings, stat grids, and runtime session documents. Runtime checkpoints +include current action-bar bindings as stable typed targets: input control +path, binding kind, item definition reference, equipment index, behavior index, +or weapon-group index. They also include current faction relationship rows as +typed corporation references plus relationship tokens and numeric standings. Player settings include player name, tutorial flag, story-file hash cursors, gameplay formatting, graphics preferences, input binding overrides, and action-bar inputs. Do not preserve `SavedGame`, Unity @@ -39,6 +39,11 @@ Unity settings, loadout templates, and run checkpoints now queue typed into canonical typed state through `AetheriaRuntimeCommitLogApplier`; the Unity-side command log is command-only and cannot become durable truth by itself. +`AetheriaRuntimeSession` is the typed Verse signal for the running state host +or one-shot pending drain. `Economy.Server` owns the long-running +`cultmesh-state-host` heartbeat and publishes `starting`, `running`, and +`stopping` states through `Aetheria.State`; drain status remains operation +telemetry, not daemon-session ownership. Unity Eve surfaces also emit typed `gamecult.eve.command.v1` documents under `GameData/aetheria-world.cc.eve.pending`. `AetheriaEveCommandBridge` is the provider-owned acceptance organ for those commands: it validates the provider, @@ -108,9 +113,10 @@ field from key 3, name-file and boss-hull legacy IDs, influence distance, allegiance count, full allegiance edges, and music bank IDs. Corporation documents are `aetheria.corporation.v2` records because runtime generation needs actual allegiance weights, not only display counts. Runtime checkpoints -now persist session faction relationships into typed run state, but broader -runtime object graphs, typed behavior factory construction, and simulation -state remain legacy until dedicated typed runtime documents exist. +now persist session faction relationships into typed run state, and runtime +sessions publish typed state-host liveness, but broader runtime object graphs, +typed behavior factory construction, and simulation state remain legacy until +dedicated typed runtime documents exist. Typed name-file documents are `aetheria.name_file.v2` records. They carry the legacy ID, display name, count, compact sample names for surfaces, and the full name array needed to move `Galaxy`/Markov name generation off legacy @@ -154,10 +160,12 @@ corporation records. `Aetheria.State.Smoke` writes and reopens full typed player settings plus a typed loadout template in addition to world, catalog, run state, zone state, and -an entity snapshot. The run smoke proves a typed run can reference a typed zone, -the zone can reference typed entity snapshots, and entity snapshots can preserve -equipment slots, weapon groups, and stat grids without reviving -`PlayerSettings.msgpack`, `.loadout`, or `.zone` serialization. +an entity snapshot. It also writes and reopens `AetheriaRuntimeSession` and +checks that the Eve provider advertises the runtime-session schema. The run +smoke proves a typed run can reference a typed zone, the zone can reference +typed entity snapshots, and entity snapshots can preserve equipment slots, +weapon groups, and stat grids without reviving `PlayerSettings.msgpack`, +`.loadout`, or `.zone` serialization. `Aetheria.State.Verify` opens a materialized state file and checks that the typed migration ledger matches the actual item, corporation, and name-file diff --git a/Economy.Server/Program.cs b/Economy.Server/Program.cs index 7d0974e06..45e444dc0 100644 --- a/Economy.Server/Program.cs +++ b/Economy.Server/Program.cs @@ -17,9 +17,12 @@ internal static class Program runtimeId: RuntimeId, startServer: true).ConfigureAwait(false); + var startedAtUtc = DateTimeOffset.UtcNow.ToString("O"); + await PublishRuntimeSessionAsync(node, startedAtUtc, "starting").ConfigureAwait(false); await EnsureWorldDocumentAsync(node).ConfigureAwait(false); await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false); await ApplyPendingEveCommandsAsync(node).ConfigureAwait(false); + await PublishRuntimeSessionAsync(node, startedAtUtc, "running").ConfigureAwait(false); await node.FlushAsync().ConfigureAwait(false); if (HasFlag(args, "--apply-pending-once")) @@ -29,8 +32,9 @@ internal static class Program } Console.WriteLine("Aetheria CultMesh state host is running. Press Ctrl+C to stop."); - await RunUntilShutdownAsync(node, PendingInterval(args)).ConfigureAwait(false); + await RunUntilShutdownAsync(node, startedAtUtc, PendingInterval(args)).ConfigureAwait(false); + await PublishRuntimeSessionAsync(node, startedAtUtc, "stopping").ConfigureAwait(false); Console.WriteLine("Aetheria CultMesh state host stopping."); } @@ -212,13 +216,31 @@ internal static class Program Status = "idle" }; var eveStatus = await node.GetEveCommandDrainStatusAsync().ConfigureAwait(false); - await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(commitStatus, eveStatus)) + var runtimeSession = await node.GetRuntimeSessionAsync(RuntimeId).ConfigureAwait(false); + await node.PutOperationsSurfaceAsync(AetheriaOperationsSurfaceProjector.Build(commitStatus, eveStatus, runtimeSession)) .ConfigureAwait(false); await node.PutProviderAdvertisementAsync( AetheriaProviderAdvertisementProjector.Build(node.StatePath, updatedAtUtc)).ConfigureAwait(false); await node.FlushAsync().ConfigureAwait(false); } + private static async Task PublishRuntimeSessionAsync( + AetheriaStateNode node, + string startedAtUtc, + string status) + { + var now = DateTimeOffset.UtcNow.ToString("O"); + await node.PutRuntimeSessionAsync(new AetheriaRuntimeSession + { + RuntimeId = RuntimeId, + Role = "cultmesh-state-host", + StartedAtUtc = startedAtUtc, + LastSeenAtUtc = now, + Status = status + }).ConfigureAwait(false); + await PublishOperationsStateAsync(node, now).ConfigureAwait(false); + } + private static int CountPendingRuntimeCommits(string statePath) { var pendingDirectory = statePath + ".pending"; @@ -235,7 +257,10 @@ internal static class Program : 0; } - private static async Task RunUntilShutdownAsync(AetheriaStateNode node, TimeSpan pendingInterval) + private static async Task RunUntilShutdownAsync( + AetheriaStateNode node, + string startedAtUtc, + TimeSpan pendingInterval) { var stopped = new TaskCompletionSource(); Console.CancelKeyPress += (_, eventArgs) => @@ -253,6 +278,7 @@ internal static class Program await ApplyPendingRuntimeCommitsAsync(node).ConfigureAwait(false); await ApplyPendingEveCommandsAsync(node).ConfigureAwait(false); + await PublishRuntimeSessionAsync(node, startedAtUtc, "running").ConfigureAwait(false); } } diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index ded6cc76c..7cff23f5a 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -725,6 +725,10 @@ First Aetheria surfaces to publish: - Done: extend run checkpoint commits and smoke coverage to carry typed faction relationship rows into `AetheriaRunState` as corporation references, relationship tokens, and numeric standings. + - Done: make `AetheriaRuntimeSession` a live CultMesh document with state + node ports, provider advertisement, server heartbeat publication, and + smoke coverage; daemon liveness is no longer only console output or + incidental drain status. - Remaining: add typed documents/mappers for runtime object graphs, typed behavior factory construction, simulation state, and any catalog fields not covered by the stable scalar/fingerprint/payload pass. @@ -1116,7 +1120,9 @@ First Aetheria surfaces to publish: - CultCache smoke proves write, flush, reopen, query, and typed reference resolution, including full player settings as the `PlayerSettings.msgpack` replacement, loadout templates as the `.loadout` replacement, and a run -> - zone -> entity snapshot graph as the `.zone` replacement. + zone -> entity snapshot graph as the `.zone` replacement. It also proves + `aetheria.runtime_session.v1` is advertised and survives reopen as the + typed daemon-session signal. - CultMesh smoke proves node start, typed put/get, subscription, flush, and reopen. - Eve surface smoke proves provider-owned surface documents are generated from