Quarantine Mimir's Eve operator surface: remove EveDashboard, EveBrowserReference and their contract documents
Parked at tag parked/Mimir-eve-surface per thing2:ruling:eve-dashboards-quarantined; rebuild on Thing 2. Mimir.EveSensorReceiver (camera frame ingest) stays. map.yaml no longer calls a live listener a tombstone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9481fbaf44
commit
43cebb6fb9
15 changed files with 19 additions and 4584 deletions
|
|
@ -44,7 +44,6 @@
|
|||
"authorities": {
|
||||
"configurationCalibrationLaunchStatus": "Mimir",
|
||||
"programComposition": "Mimir",
|
||||
"operatorSurfaces": "Eve",
|
||||
"sitePublication": "Yggdrasil publisher daemon",
|
||||
"denseVisualFusion": "Fensalir",
|
||||
"gpuMaterialAndBrushReconciliation": "Fensalir",
|
||||
|
|
@ -97,10 +96,8 @@
|
|||
]
|
||||
},
|
||||
"operator": {
|
||||
"owner": "Mimir/Eve",
|
||||
"owner": "Mimir",
|
||||
"surfaces": [
|
||||
"eve-gui-compositor",
|
||||
"eve-tui-operator",
|
||||
"cultmesh-command-surface"
|
||||
]
|
||||
},
|
||||
|
|
@ -120,7 +117,6 @@
|
|||
"native-reservoir",
|
||||
"Fensalir",
|
||||
"Faust",
|
||||
"Eve",
|
||||
"Yggdrasil publisher daemon"
|
||||
],
|
||||
"neverInCompatibilityEdges": [
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ Inputs become outputs like this:
|
|||
- OBS can receive ordinary media-source URLs for compatibility; it does not need
|
||||
a plugin for this bridge path.
|
||||
- Audio remains separately mixable at the compatibility edge, but the durable
|
||||
control surface belongs to Mimir/Eve.
|
||||
control surface belongs to Mimir.
|
||||
- Ports are stable and derived from one base port plus explicit offsets.
|
||||
- Secrets, if used, stay in local config and are not committed.
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ Timestamp for this pass: 2026-05-23, starting at 23:24 Europe/London.
|
|||
Mimir collects local and network audio/video samples into bounded rolling
|
||||
runtime buffers, uses active/passive timing evidence to place audio streams on a
|
||||
canonical timeline, and lets Mimir own program composition while Fensalir lowers
|
||||
pixels, Faust/native DSP moves audio, Eve exposes operator surfaces, and
|
||||
pixels, Faust/native DSP moves audio, and
|
||||
Yggdrasil publishes the public feed.
|
||||
|
||||
## Current Mechanism
|
||||
|
|
@ -61,8 +61,6 @@ reflection taps.
|
|||
program video output.
|
||||
- Faust/native DSP will own hot resampling, fractional delay, separation,
|
||||
spatialization, and stems.
|
||||
- Eve GUI/TUI lowerers render Mimir operator surfaces without owning scene
|
||||
truth.
|
||||
- The Yggdrasil-facing publisher daemon consumes Mimir program output without
|
||||
owning composition.
|
||||
- OBS is a compatibility sink only.
|
||||
|
|
@ -91,7 +89,6 @@ flowchart TD
|
|||
Probes["native/probes/*"] --> FrameEvents["MimirFrameEventProcessStreamSource"]
|
||||
FrameEvents --> Sources
|
||||
Runtime --> Scene["Mimir program scene graph"]
|
||||
Scene --> Eve["Eve operator surfaces"]
|
||||
Scene --> Publisher["Yggdrasil site publisher"]
|
||||
Scene -. "compatibility" .-> Obs["OBS adapter"]
|
||||
Reservoir["native/reservoir"] -. "future lower ABI" .-> Hub
|
||||
|
|
@ -108,7 +105,7 @@ flowchart TD
|
|||
- `native/probes/*` are measurement and diagnostic tools.
|
||||
- `scripts/*` are bridge utilities, not the final synchronized program path.
|
||||
- `docs/mimir-program-composition.md` is the authority map for Muninn capture
|
||||
hosts, Mimir composition, Eve operation, Yggdrasil publication, and OBS
|
||||
hosts, Mimir composition, Yggdrasil publication, and OBS
|
||||
compatibility.
|
||||
|
||||
## Stage 0: Solution And App Host
|
||||
|
|
|
|||
|
|
@ -365,14 +365,12 @@ a named invariant that the native runtime cannot protect yet.
|
|||
- Windows bridge scripts for sender discovery/start/stop and simple OBS Media
|
||||
Source compatibility ingest.
|
||||
- `src/Mimir.Runtime/Synchronization` now contains the first typed program
|
||||
composition contracts: `mimir.program_scene.v1`, `mimir.program_output.v1`,
|
||||
and `mimir.eve_operator_surface.v1`. These make Mimir's scene graph,
|
||||
Yggdrasil/site publication route, and Eve operator control/preview surface
|
||||
composition contracts: `mimir.program_scene.v1` and `mimir.program_output.v1`.
|
||||
These make Mimir's scene graph and Yggdrasil/site publication route
|
||||
manifestable through CultMesh.
|
||||
- `docs/mimir-program-composition.md` is the current authority map for the new
|
||||
stream-program architecture: Muninn observes on Starfire/Nightwing/Raven-class
|
||||
hosts, Mimir consumes selected streams and composes the program, Eve lowers
|
||||
controls/previews/stats, Yggdrasil publishes to the site, and OBS is
|
||||
hosts, Mimir consumes selected streams and composes the program, Yggdrasil publishes to the site, and OBS is
|
||||
compatibility-only.
|
||||
- `src/Mimir.CultMeshMedia` is the first explicit CultMesh media/body bridge:
|
||||
`relay` hosts the document relay on CultNet RUDP `3075`, `send` reads an
|
||||
|
|
@ -399,23 +397,13 @@ a named invariant that the native runtime cannot protect yet.
|
|||
capture still defaults to FFmpeg desktop frames plus Mimir's WASAPI loopback
|
||||
capture muxed as H.264/AAC MPEG-TS; DirectShow audio remains an explicit
|
||||
fallback.
|
||||
- `src/Mimir.EveDashboard` is archived until the dashboard returns as a pure
|
||||
CultMesh/Odin publisher and Eve lowering. The old HTTP/WebSocket deck broker,
|
||||
local health route, socket command channel, and provider route catalog are not
|
||||
daemon transport. Dashboard state must be published as typed CultMesh/Eve
|
||||
documents through Odin, and dashboard commands must arrive as typed Odin/
|
||||
CultMesh command documents.
|
||||
- `src/Mimir.EveBrowserReference` serves static browser lowerings and can
|
||||
publish its own `idunn.daemon_health` record over
|
||||
`cultnet.transport.rudp.v0`. It remains a renderer/reference surface, not
|
||||
program authority.
|
||||
- Documentation for OBS receiver setup, native rebuild boundaries, the viable
|
||||
stream app, and the Mimir Face.
|
||||
|
||||
## Temporary
|
||||
|
||||
- Audio and video may still traverse separate OBS/SRT endpoints during bridge
|
||||
testing, but Mimir/Eve owns independent controls.
|
||||
testing, but Mimir owns independent controls.
|
||||
- The CultMesh media bridge still lowers to local UDP for compatibility sinks
|
||||
because OBS is not a CultMesh consumer. Network transit between Raven,
|
||||
Yggdrasil, and Starfire is the Odin-discovered CultMesh/CultNet path;
|
||||
|
|
@ -533,7 +521,7 @@ proxy impairment profiles and long soak remain the completion gate.
|
|||
9. Implement the Mimir program scene graph as the shared commit primitive for
|
||||
source subscription, transforms, crop, chroma key, visibility, layer order,
|
||||
preview, and output publication. Import the current OBS scene only as an
|
||||
initial mirror, then make Eve GUI/TUI the operator surface.
|
||||
initial mirror; the operator surface returns on Thing 2.
|
||||
10. Add the Yggdrasil-facing site publisher daemon that consumes the Mimir
|
||||
program output and publishes it without owning a second composition.
|
||||
11. Lower `AquariumBufferFieldFrame` spline tube fields into Fensalir compute:
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ distributed capture surfaces into one realtime coherent volumetric field. The
|
|||
field is the product. The bridge scripts are scaffolding and probes.
|
||||
|
||||
The live target is one native five-second rolling reservoir feeding Mimir-owned
|
||||
program composition, Fensalir GPU fusion, Faust/native DSP, Eve operator
|
||||
program composition, Fensalir GPU fusion, Faust/native DSP, operator
|
||||
surfaces, and a Yggdrasil site publisher. OBS is compatibility only. Retention
|
||||
has one owner. Typed views are indexes, not private histories.
|
||||
|
||||
|
|
@ -70,7 +70,6 @@ Invariants:
|
|||
program texture output.
|
||||
- Faust/native DSP owns hot audio alignment, room suppression, voice separation,
|
||||
stems, and spatial bed generation.
|
||||
- Eve lowerers render operator controls and preview without owning scene truth.
|
||||
- The Yggdrasil publisher daemon ships the Mimir program to the site without
|
||||
owning composition.
|
||||
- FFmpeg owns simple capture, encode, and transport in V1 bridge mode.
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ temporary compatibility sink only.
|
|||
- Owner: Mimir owns the program scene graph and subscription policy.
|
||||
- Inputs: Muninn-published local and remote monitor, window, camera, audio,
|
||||
loopback, and health streams.
|
||||
- Outputs: a Mimir program video surface, aligned audio stems or final bus, Eve
|
||||
GUI/TUI operator surfaces, and a Yggdrasil-facing site publication feed.
|
||||
- Outputs: a Mimir program video surface, aligned audio stems or final bus,
|
||||
and a Yggdrasil-facing site publication feed.
|
||||
- Derived state: OBS receives a compatibility feed when useful; it does not own
|
||||
scene truth, broadcast truth, preview truth, stats, source selection, or
|
||||
publication.
|
||||
- Forbidden writers: OBS scenes, browser dashboards, site daemons, and Muninn
|
||||
capture hosts must not independently decide the program composition.
|
||||
- Shared paths: direct operator edits, remote Eve commands, preset loads,
|
||||
- Shared paths: direct operator edits, remote commands, preset loads,
|
||||
calibration-driven source selection, and recovery scripts must commit through
|
||||
the same Mimir scene graph command surface.
|
||||
- Deletion line: old OBS-owned composition docs and config names are demoted to
|
||||
|
|
@ -39,7 +39,6 @@ flowchart TD
|
|||
F --> I["Faust/native DSP"]
|
||||
H --> J["program video surface"]
|
||||
I --> K["program audio stems/bus"]
|
||||
G --> L["Eve GUI/TUI operator surfaces"]
|
||||
J --> M["Yggdrasil site publisher daemon"]
|
||||
K --> M
|
||||
M --> N["GameCult site"]
|
||||
|
|
@ -68,22 +67,7 @@ The current OBS scene can be mirrored as an initial Mimir scene:
|
|||
active.
|
||||
|
||||
After import, OBS is not the editor of record. It may display the Mimir program
|
||||
feed, but operator edits happen through Mimir/Eve command surfaces.
|
||||
|
||||
## Eve Operator Surface
|
||||
|
||||
Mimir publishes Eve GUI/TUI documents for:
|
||||
|
||||
- live program preview;
|
||||
- source catalog and subscription state;
|
||||
- layer transform, crop, chroma key, visibility, and ordering;
|
||||
- audio stem gain/mute/route controls;
|
||||
- stream health, latency, buffer depth, timing confidence, and publication
|
||||
status;
|
||||
- emergency scene selection and publisher recovery commands.
|
||||
|
||||
Eve lowerers render those controls on any device without owning the underlying
|
||||
scene graph.
|
||||
feed, but operator edits happen through Mimir command surfaces.
|
||||
|
||||
## Site Publisher
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ flowchart TD
|
|||
N --> F["Faust/native DSP"]
|
||||
E --> G["Mimir program video"]
|
||||
F --> H["program stems + spatial bed"]
|
||||
E --> I["Eve GUI/TUI operator surfaces"]
|
||||
G --> J["Yggdrasil site publisher"]
|
||||
H --> J
|
||||
G -. "compatibility" .-> K["OBS adapter"]
|
||||
|
|
@ -53,17 +52,10 @@ Ownership:
|
|||
Fensalir-facing wand pose stream.
|
||||
- Mimir owns program composition, source subscription policy, preview/control
|
||||
state, stats, and publication intent.
|
||||
- Mimir's old Eve dashboard broker is archived; it does not publish health,
|
||||
provider catalogs, or command surfaces while the socket path is cut. The
|
||||
browser reference remains a client lowering, not daemon truth. Dashboard state
|
||||
must return as typed CultMesh/Eve documents through Odin; product/debug render
|
||||
surfaces are client lowerings and compatibility evidence, not daemon truth.
|
||||
- Fensalir owns dense visual fusion, material/brush/splat reconciliation,
|
||||
D3D12 interop, runtime UI lowering, and local program texture output.
|
||||
- Faust/native DSP owns hot audio alignment, suppression, separation,
|
||||
spatialization, and stem generation.
|
||||
- Eve GUI/TUI lowerers render Mimir's operator surfaces on any device without
|
||||
owning scene truth.
|
||||
- The Yggdrasil-facing publisher daemon consumes Mimir program output and
|
||||
publishes it to the site without owning composition.
|
||||
- OBS is a temporary compatibility sink, not a composition or broadcast
|
||||
|
|
@ -302,7 +294,7 @@ evidence.
|
|||
authority map for stream-program output. Muninn runs on Starfire, Nightwing, and
|
||||
Raven-class capture hosts to publish local stream capabilities and selected
|
||||
media bodies. Mimir consumes only the streams it needs for calibration and
|
||||
composition, commits one typed scene graph, emits Eve GUI/TUI operator surfaces,
|
||||
composition, commits one typed scene graph,
|
||||
and publishes program output locally plus through a Yggdrasil-facing site
|
||||
publisher.
|
||||
|
||||
|
|
@ -439,14 +431,6 @@ an RUDP contract exists.
|
|||
|
||||
Mimir's current daemon-health publishers are local RUDP witnesses:
|
||||
|
||||
- `src/Mimir.EveDashboard` is a fail-closed archive tombstone. It no longer
|
||||
starts a TCP listener, serves `/eve/deck`, accepts WebSocket commands, or
|
||||
publishes daemon health. `mimir.eve_dashboard_state` must return through Odin/
|
||||
CultMesh state publication before this service can be redeployed.
|
||||
- `src/Mimir.EveBrowserReference` serves a static browser lowering and, when
|
||||
configured with `--idunn-rudp-health` or
|
||||
`MIMIR_EVE_BROWSER_REFERENCE_IDUNN_RUDP_HEALTH`, sends its own
|
||||
`idunn.daemon_health` record over the same RUDP document path.
|
||||
|
||||
Invariant: health publication is not ownership. The daemon reports what it can
|
||||
honestly observe about itself; Idunn decides keepalive action through the
|
||||
|
|
|
|||
|
|
@ -1477,10 +1477,6 @@ static int RunPerfectMachineProfileSmoke()
|
|||
"mimir-site-program",
|
||||
scene.SceneId,
|
||||
MimirProgramPublicationConfigurations.YggdrasilSiteProgram);
|
||||
var eveSurface = MimirCultMeshContractFactory.CreateOperatorSurface(
|
||||
"mimir-eve-gui-compositor",
|
||||
scene.SceneId,
|
||||
MimirProgramPublicationConfigurations.OperatorSurfaces[0]);
|
||||
var observations = new[]
|
||||
{
|
||||
new MimirBioacousticWordObservation(0, MimirBioacousticTimeline.Default.EventForIndex(0).StartSeconds * MimirBioacousticTimeline.SampleRate + 317.375, 0.95),
|
||||
|
|
@ -1565,7 +1561,7 @@ static int RunPerfectMachineProfileSmoke()
|
|||
Console.WriteLine(
|
||||
$"perfect-machine-actuator source={actuator.SourceId} delay={actuator.TargetDelaySamples:0.000} ratio={actuator.ResampleRatio:0.000000000} controls={actuator.FaustControls.Length}");
|
||||
Console.WriteLine(
|
||||
$"perfect-machine-program scene={scene.SceneId} layers={scene.Layers.Length} output={output.OutputId} route={output.PublisherRoute} eveCommands={eveSurface.CommandTopics.Length}");
|
||||
$"perfect-machine-program scene={scene.SceneId} layers={scene.Layers.Length} output={output.OutputId} route={output.PublisherRoute}");
|
||||
Console.WriteLine(
|
||||
$"perfect-machine-clock anchors={clock?.AnchorCount ?? 0} coverage={clock?.AnchorCoverage ?? 0:0.000} offset={clock?.SourceOffsetSamples ?? double.NaN:0.000} confidence={clock?.Confidence ?? 0:0.000}");
|
||||
Console.WriteLine(
|
||||
|
|
@ -3927,11 +3923,6 @@ static async Task<int> RunPerfectMachineContractSmokeAsync(string outputPath)
|
|||
scene.SceneId,
|
||||
MimirProgramPublicationConfigurations.YggdrasilSiteProgram,
|
||||
createdAt);
|
||||
var eveSurface = MimirCultMeshContractFactory.CreateOperatorSurface(
|
||||
"mimir-eve-gui-compositor",
|
||||
scene.SceneId,
|
||||
MimirProgramPublicationConfigurations.OperatorSurfaces[0],
|
||||
createdAt);
|
||||
var starfireMove = MimirTrackingObservation.PsMove(
|
||||
"starfire:move:primary",
|
||||
"psmove:starfire:00",
|
||||
|
|
@ -4004,10 +3995,6 @@ static async Task<int> RunPerfectMachineContractSmokeAsync(string outputPath)
|
|||
output,
|
||||
new CultRecordHandle<MimirProgramOutputDocument>(new CultRecordKey($"mimir-program-output:{output.OutputId}")))
|
||||
.ConfigureAwait(false);
|
||||
await cache.UpsertAsync(
|
||||
eveSurface,
|
||||
new CultRecordHandle<MimirEveOperatorSurfaceDocument>(new CultRecordKey($"mimir-eve-surface:{eveSurface.SurfaceId}")))
|
||||
.ConfigureAwait(false);
|
||||
await cache.UpsertAsync(
|
||||
starfireMove,
|
||||
new CultRecordHandle<MimirTrackingObservation>(new CultRecordKey($"mimir-move-tracking:{starfireMove.ObservationId}")))
|
||||
|
|
@ -4023,7 +4010,7 @@ static async Task<int> RunPerfectMachineContractSmokeAsync(string outputPath)
|
|||
await cache.FlushAsync().ConfigureAwait(false);
|
||||
|
||||
Console.WriteLine(
|
||||
$"perfect-machine-contract-smoke path={outputPath} codebookMotifs={codebook.Motifs.Length} decoder={decoder.Configuration.Id} pathBands={pathState.BandResponses.Length} actuatorControls={actuator.FaustControls.Length} sceneLayers={scene.Layers.Length} programOutput={output.OutputId} eveCommands={eveSurface.CommandTopics.Length} moveTracking=2 fusedMovePose={fusedMovePose.PoseId} authority={fusedMovePose.FusionAuthorityId} consumer={fusedMovePose.ConsumerContract} hosts={starfireMove.HostId},{nightwingMove.HostId}");
|
||||
$"perfect-machine-contract-smoke path={outputPath} codebookMotifs={codebook.Motifs.Length} decoder={decoder.Configuration.Id} pathBands={pathState.BandResponses.Length} actuatorControls={actuator.FaustControls.Length} sceneLayers={scene.Layers.Length} programOutput={output.OutputId} moveTracking=2 fusedMovePose={fusedMovePose.PoseId} authority={fusedMovePose.FusionAuthorityId} consumer={fusedMovePose.ConsumerContract} hosts={starfireMove.HostId},{nightwingMove.HostId}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -4045,11 +4032,6 @@ static async Task<int> ImportObsProgramSceneAsync(string inputPath, string scene
|
|||
scene.SceneId,
|
||||
MimirProgramPublicationConfigurations.YggdrasilSiteProgram,
|
||||
importedAt);
|
||||
var eveSurface = MimirCultMeshContractFactory.CreateOperatorSurface(
|
||||
"mimir-eve-gui-compositor",
|
||||
scene.SceneId,
|
||||
MimirProgramPublicationConfigurations.OperatorSurfaces[0],
|
||||
importedAt);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(outputPath)) ?? ".");
|
||||
using var cache = await CultCacheMessagePack.OpenAsync(outputPath).ConfigureAwait(false);
|
||||
|
|
@ -4061,10 +4043,6 @@ static async Task<int> ImportObsProgramSceneAsync(string inputPath, string scene
|
|||
output,
|
||||
new CultRecordHandle<MimirProgramOutputDocument>(new CultRecordKey($"mimir-program-output:{output.OutputId}")))
|
||||
.ConfigureAwait(false);
|
||||
await cache.UpsertAsync(
|
||||
eveSurface,
|
||||
new CultRecordHandle<MimirEveOperatorSurfaceDocument>(new CultRecordKey($"mimir-eve-surface:{eveSurface.SurfaceId}")))
|
||||
.ConfigureAwait(false);
|
||||
await cache.FlushAsync().ConfigureAwait(false);
|
||||
|
||||
var visible = scene.Layers.Count(layer => layer.Visible);
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AssemblyName>Mimir.EveBrowserReference</AssemblyName>
|
||||
<RootNamespace>Mimir.EveBrowserReference</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Networking\GameCult.Networking.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -1,734 +0,0 @@
|
|||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using GameCult.Caching;
|
||||
using GameCult.Caching.MessagePack;
|
||||
using GameCult.Networking;
|
||||
using MessagePack;
|
||||
|
||||
var port = ParseInt(args, "--port", 8891);
|
||||
var bind = ParseString(args, "--bind", Environment.GetEnvironmentVariable("MIMIR_EVE_BROWSER_REFERENCE_BIND") ?? "0.0.0.0");
|
||||
var directory = ParseString(args, "--directory", Environment.GetEnvironmentVariable("MIMIR_EVE_BROWSER_REFERENCE_DIRECTORY") ?? Directory.GetCurrentDirectory());
|
||||
var cultCacheWitnessPath = ParseString(
|
||||
args,
|
||||
"--cultcache-witness",
|
||||
Environment.GetEnvironmentVariable("MIMIR_EVE_BROWSER_REFERENCE_CULTCACHE_WITNESS") ?? DefaultPaths.CultCacheWitnessPath);
|
||||
var idunnRudpHealth = ParseString(args, "--idunn-rudp-health", Environment.GetEnvironmentVariable("MIMIR_EVE_BROWSER_REFERENCE_IDUNN_RUDP_HEALTH") ?? "");
|
||||
var idunnDaemon = ParseString(args, "--idunn-daemon", Environment.GetEnvironmentVariable("MIMIR_EVE_BROWSER_REFERENCE_IDUNN_DAEMON") ?? "nightwing-eve-browser-reference");
|
||||
var idunnHealthContract = ParseString(args, "--idunn-health-contract", Environment.GetEnvironmentVariable("MIMIR_EVE_BROWSER_REFERENCE_IDUNN_HEALTH_CONTRACT") ?? "nightwing.cultnet-rudp-browser-reference-health");
|
||||
|
||||
using var verseRuntime = EveBrowserReferenceVerseRuntime.Create(cultCacheWitnessPath);
|
||||
using var server = new EveBrowserReferenceServer(
|
||||
IPAddress.Parse(bind),
|
||||
port,
|
||||
Path.GetFullPath(directory),
|
||||
verseRuntime,
|
||||
new IdunnRudpHealthOptions(idunnRudpHealth, idunnDaemon, idunnHealthContract));
|
||||
await server.RunAsync().ConfigureAwait(false);
|
||||
|
||||
static int ParseInt(IReadOnlyList<string> args, string name, int fallback)
|
||||
{
|
||||
for (var index = 0; index < args.Count - 1; index++)
|
||||
{
|
||||
if (string.Equals(args[index], name, StringComparison.OrdinalIgnoreCase) &&
|
||||
int.TryParse(args[index + 1], CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
static string ParseString(IReadOnlyList<string> args, string name, string fallback)
|
||||
{
|
||||
for (var index = 0; index < args.Count - 1; index++)
|
||||
{
|
||||
if (string.Equals(args[index], name, StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.IsNullOrWhiteSpace(args[index + 1]))
|
||||
{
|
||||
return args[index + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
|
||||
internal static class DefaultPaths
|
||||
{
|
||||
public static string CultCacheWitnessPath =>
|
||||
OperatingSystem.IsWindows()
|
||||
? @"E:\Projects\Mimir\state\eve-browser-reference.service.cc"
|
||||
: "/var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc";
|
||||
}
|
||||
|
||||
internal sealed class EveBrowserReferenceServer(
|
||||
IPAddress bindAddress,
|
||||
int port,
|
||||
string documentRoot,
|
||||
EveBrowserReferenceVerseRuntime verseRuntime,
|
||||
IdunnRudpHealthOptions idunnRudpHealth) : IDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly TcpListener listener = new(bindAddress, port);
|
||||
private readonly CancellationTokenSource stopping = new();
|
||||
private readonly object idunnRudpPublishLock = new();
|
||||
private string idunnRudpPublishStatus = string.IsNullOrWhiteSpace(idunnRudpHealth.Endpoint) ? "disabled" : "idle";
|
||||
private string idunnRudpPublishError = "";
|
||||
private string idunnRudpPublishObservedAt = "";
|
||||
private bool idunnRudpPublishInFlight;
|
||||
private DateTimeOffset lastHealthPublishAttemptAt = DateTimeOffset.MinValue;
|
||||
private DateTimeOffset lastWitnessPublishAttemptAt = DateTimeOffset.MinValue;
|
||||
|
||||
public async Task RunAsync()
|
||||
{
|
||||
if (!Directory.Exists(documentRoot))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Eve browser reference document root does not exist: {documentRoot}");
|
||||
}
|
||||
|
||||
listener.Start();
|
||||
Console.WriteLine($"Mimir Eve browser reference serving {documentRoot} on http://{bindAddress}:{port}/");
|
||||
Console.WriteLine(string.IsNullOrWhiteSpace(idunnRudpHealth.Endpoint)
|
||||
? "Mimir Eve browser reference Idunn RUDP health publisher disabled."
|
||||
: $"Mimir Eve browser reference Idunn RUDP health publisher targeting {idunnRudpHealth.Endpoint} as {idunnRudpHealth.DaemonId}.");
|
||||
Console.WriteLine($"Mimir Eve browser reference witness store publishing to {verseRuntime.WitnessPath}");
|
||||
_ = Task.Run(PublishHealthLoopAsync);
|
||||
while (!stopping.IsCancellationRequested)
|
||||
{
|
||||
var client = await listener.AcceptTcpClientAsync(stopping.Token).ConfigureAwait(false);
|
||||
_ = Task.Run(() => HandleAsync(client));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishHealthLoopAsync()
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||
while (!stopping.IsCancellationRequested && await timer.WaitForNextTickAsync(stopping.Token).ConfigureAwait(false))
|
||||
{
|
||||
var observedAt = DateTimeOffset.UtcNow;
|
||||
PublishWitnessIfDue(observedAt);
|
||||
QueueIdunnRudpHealthIfDue(observedAt);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleAsync(TcpClient tcpClient)
|
||||
{
|
||||
await using var stream = tcpClient.GetStream();
|
||||
var request = await ReadHttpRequestAsync(stream).ConfigureAwait(false);
|
||||
if (request.Path == "/health")
|
||||
{
|
||||
var health = JsonSerializer.Serialize(new
|
||||
{
|
||||
ok = true,
|
||||
documentRoot,
|
||||
port,
|
||||
transport = "static-http-lowering",
|
||||
cultCacheWitness = verseRuntime.GetWitnessStatus(),
|
||||
idunnRudpHealth = GetIdunnRudpHealthStatus(),
|
||||
}, JsonOptions);
|
||||
await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(health)).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var filePath = ResolveStaticFilePath(request.Path);
|
||||
if (filePath == null)
|
||||
{
|
||||
await WriteHttpResponseAsync(stream, "404 Not Found", "text/plain", Encoding.UTF8.GetBytes("not found")).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = await File.ReadAllBytesAsync(filePath, stopping.Token).ConfigureAwait(false);
|
||||
await WriteHttpResponseAsync(stream, "200 OK", ContentType(filePath), bytes).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private IdunnRudpHealthStatus GetIdunnRudpHealthStatus()
|
||||
{
|
||||
lock (idunnRudpPublishLock)
|
||||
{
|
||||
return new IdunnRudpHealthStatus(
|
||||
idunnRudpHealth.Endpoint,
|
||||
idunnRudpHealth.DaemonId,
|
||||
idunnRudpHealth.HealthContract,
|
||||
idunnRudpPublishStatus,
|
||||
idunnRudpPublishError,
|
||||
idunnRudpPublishInFlight,
|
||||
idunnRudpPublishObservedAt);
|
||||
}
|
||||
}
|
||||
|
||||
private void PublishWitnessIfDue(DateTimeOffset observedAt)
|
||||
{
|
||||
if (lastWitnessPublishAttemptAt != DateTimeOffset.MinValue &&
|
||||
observedAt - lastWitnessPublishAttemptAt < TimeSpan.FromSeconds(5))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lastWitnessPublishAttemptAt = observedAt;
|
||||
verseRuntime.Publish(
|
||||
CreateDaemonHealthRecord(observedAt),
|
||||
documentRoot,
|
||||
port,
|
||||
observedAt);
|
||||
}
|
||||
|
||||
private void QueueIdunnRudpHealthIfDue(DateTimeOffset observedAt)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(idunnRudpHealth.Endpoint))
|
||||
{
|
||||
idunnRudpPublishStatus = "disabled";
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastHealthPublishAttemptAt != DateTimeOffset.MinValue &&
|
||||
observedAt - lastHealthPublishAttemptAt < TimeSpan.FromSeconds(5))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var health = CreateDaemonHealthRecord(observedAt);
|
||||
var observedAtText = health.ObservedAt;
|
||||
|
||||
lock (idunnRudpPublishLock)
|
||||
{
|
||||
if (idunnRudpPublishInFlight)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
idunnRudpPublishInFlight = true;
|
||||
lastHealthPublishAttemptAt = observedAt;
|
||||
idunnRudpPublishObservedAt = observedAtText;
|
||||
if (!string.Equals(idunnRudpPublishStatus, "published", StringComparison.Ordinal))
|
||||
{
|
||||
idunnRudpPublishStatus = "publishing";
|
||||
idunnRudpPublishError = "";
|
||||
}
|
||||
}
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
IdunnRudpHealthPublisher.Publish(idunnRudpHealth.Endpoint, health);
|
||||
lock (idunnRudpPublishLock)
|
||||
{
|
||||
idunnRudpPublishStatus = "published";
|
||||
idunnRudpPublishError = "";
|
||||
idunnRudpPublishInFlight = false;
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
lock (idunnRudpPublishLock)
|
||||
{
|
||||
if (!string.Equals(idunnRudpPublishStatus, "published", StringComparison.Ordinal))
|
||||
{
|
||||
idunnRudpPublishStatus = "publish-error";
|
||||
}
|
||||
|
||||
idunnRudpPublishError = error.Message;
|
||||
idunnRudpPublishInFlight = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private IdunnDaemonHealthRecord CreateDaemonHealthRecord(DateTimeOffset observedAt)
|
||||
{
|
||||
var observedAtText = observedAt.ToString("O", CultureInfo.InvariantCulture);
|
||||
return new IdunnDaemonHealthRecord
|
||||
{
|
||||
DaemonId = idunnRudpHealth.DaemonId,
|
||||
State = "healthy",
|
||||
Detail = $"Nightwing Eve browser reference serving static lowering; root={documentRoot}; port={port}; witness={verseRuntime.WitnessPath}",
|
||||
ObservedAt = observedAtText,
|
||||
HealthContract = idunnRudpHealth.HealthContract,
|
||||
PublicationSource = "daemon-published",
|
||||
Transport = "cultnet.transport.rudp.v0",
|
||||
};
|
||||
}
|
||||
|
||||
private string? ResolveStaticFilePath(string path)
|
||||
{
|
||||
var requestPath = path.Split('?', 2)[0];
|
||||
if (requestPath == "/")
|
||||
{
|
||||
requestPath = "/index.html";
|
||||
}
|
||||
|
||||
var relativePath = Uri.UnescapeDataString(requestPath.TrimStart('/'))
|
||||
.Replace('/', Path.DirectorySeparatorChar);
|
||||
var candidate = Path.GetFullPath(Path.Combine(documentRoot, relativePath));
|
||||
if (!candidate.StartsWith(documentRoot.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, StringComparison.Ordinal) &&
|
||||
!string.Equals(candidate, documentRoot, StringComparison.Ordinal))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return File.Exists(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
private static string ContentType(string path) =>
|
||||
Path.GetExtension(path).ToLowerInvariant() switch
|
||||
{
|
||||
".html" => "text/html; charset=utf-8",
|
||||
".css" => "text/css; charset=utf-8",
|
||||
".js" => "application/javascript; charset=utf-8",
|
||||
".json" => "application/json; charset=utf-8",
|
||||
".png" => "image/png",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".svg" => "image/svg+xml",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
|
||||
private static async Task<HttpRequest> ReadHttpRequestAsync(NetworkStream stream)
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
var length = await stream.ReadAsync(buffer).ConfigureAwait(false);
|
||||
if (length == 0)
|
||||
{
|
||||
return new HttpRequest("/");
|
||||
}
|
||||
|
||||
var text = Encoding.ASCII.GetString(buffer, 0, length);
|
||||
var firstLineEnd = text.IndexOf("\r\n", StringComparison.Ordinal);
|
||||
var firstLine = firstLineEnd >= 0 ? text[..firstLineEnd] : text;
|
||||
var parts = firstLine.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
return new HttpRequest(parts.Length >= 2 ? parts[1] : "/");
|
||||
}
|
||||
|
||||
private static async Task WriteHttpResponseAsync(NetworkStream stream, string status, string contentType, byte[] body)
|
||||
{
|
||||
var header = Encoding.ASCII.GetBytes(
|
||||
$"HTTP/1.1 {status}\r\nContent-Type: {contentType}\r\nContent-Length: {body.Length}\r\nConnection: close\r\n\r\n");
|
||||
await stream.WriteAsync(header).ConfigureAwait(false);
|
||||
await stream.WriteAsync(body).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
stopping.Cancel();
|
||||
listener.Stop();
|
||||
stopping.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record HttpRequest(string Path);
|
||||
|
||||
internal sealed record IdunnRudpHealthOptions(string Endpoint, string DaemonId, string HealthContract);
|
||||
|
||||
internal sealed record CultCacheWitnessStatus(
|
||||
string Path,
|
||||
string Status,
|
||||
string Error,
|
||||
string ObservedAtUtc,
|
||||
IReadOnlyList<string> PublishedSchemas);
|
||||
|
||||
internal sealed record IdunnRudpHealthStatus(
|
||||
string Endpoint,
|
||||
string Daemon,
|
||||
string Contract,
|
||||
string Status,
|
||||
string Error,
|
||||
bool InFlight,
|
||||
string ObservedAtUtc);
|
||||
|
||||
[CultDocument("idunn.daemon_health", "idunn.daemon_health.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class IdunnDaemonHealthRecord
|
||||
{
|
||||
[Key(0)] public string DaemonId { get; set; } = "nightwing-eve-browser-reference";
|
||||
[Key(1)] public string State { get; set; } = "active";
|
||||
[Key(2)] public string Detail { get; set; } = "";
|
||||
[Key(3)] public string ObservedAt { get; set; } = "";
|
||||
[Key(4)] public string HealthContract { get; set; } = "nightwing.cultnet-rudp-browser-reference-health";
|
||||
[Key(5)] public string PublicationSource { get; set; } = "daemon-published";
|
||||
[Key(6)] public string Transport { get; set; } = "cultnet.transport.rudp.v0";
|
||||
}
|
||||
|
||||
internal sealed class EveBrowserReferenceVerseRuntime : IDisposable
|
||||
{
|
||||
private static readonly CultRecordKey ManifestKey = new("nightwing-eve-browser-reference");
|
||||
private static readonly CultRecordKey StaticSurfaceKey = new("nightwing-eve-browser-reference");
|
||||
private static readonly CultRecordKey CommandBoundaryKey = new("nightwing-eve-browser-reference");
|
||||
private static readonly CultRecordKey TransportProfileKey = new("nightwing-eve-browser-reference");
|
||||
private static readonly string[] PublishedSchemas =
|
||||
[
|
||||
"mimir.eve_browser_reference_manifest.v1",
|
||||
"mimir.eve_browser_reference_static_surface.v1",
|
||||
"mimir.eve_browser_reference_command_boundary.v1",
|
||||
"mimir.eve_browser_reference_transport_profile.v1",
|
||||
"idunn.daemon_health.v1",
|
||||
];
|
||||
|
||||
private readonly CultDocumentRegistry registry = new();
|
||||
private readonly string witnessPath;
|
||||
private readonly object publishLock = new();
|
||||
private string witnessStatus = "starting";
|
||||
private string witnessError = string.Empty;
|
||||
private string witnessObservedAtUtc = string.Empty;
|
||||
|
||||
private EveBrowserReferenceVerseRuntime(string witnessPath)
|
||||
{
|
||||
this.witnessPath = Path.GetFullPath(witnessPath);
|
||||
}
|
||||
|
||||
public string WitnessPath => witnessPath;
|
||||
|
||||
public static EveBrowserReferenceVerseRuntime Create(string witnessPath)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(witnessPath)) ?? ".");
|
||||
return new EveBrowserReferenceVerseRuntime(witnessPath);
|
||||
}
|
||||
|
||||
public CultCacheWitnessStatus GetWitnessStatus()
|
||||
{
|
||||
lock (publishLock)
|
||||
{
|
||||
return new CultCacheWitnessStatus(
|
||||
witnessPath,
|
||||
witnessStatus,
|
||||
witnessError,
|
||||
witnessObservedAtUtc,
|
||||
PublishedSchemas);
|
||||
}
|
||||
}
|
||||
|
||||
public void Publish(
|
||||
IdunnDaemonHealthRecord healthRecord,
|
||||
string documentRoot,
|
||||
int port,
|
||||
DateTimeOffset observedAt)
|
||||
{
|
||||
var observedAtUtc = observedAt.ToString("O", CultureInfo.InvariantCulture);
|
||||
|
||||
try
|
||||
{
|
||||
lock (publishLock)
|
||||
{
|
||||
var records = new List<CultPersistedRecord>();
|
||||
var schemas = new Dictionary<string, CultSchemaCatalogEntry>(StringComparer.Ordinal);
|
||||
var assets = EnumerateRelativeFiles(documentRoot);
|
||||
var fixtures = assets
|
||||
.Where(path => path.StartsWith("fixtures/", StringComparison.Ordinal))
|
||||
.ToArray();
|
||||
|
||||
AddRecord(records, schemas, ToManifestDocument(documentRoot, port, assets), ManifestKey, observedAtUtc);
|
||||
AddRecord(records, schemas, ToStaticSurfaceDocument(documentRoot, assets, fixtures, observedAtUtc), StaticSurfaceKey, observedAtUtc);
|
||||
AddRecord(records, schemas, ToCommandBoundaryDocument(observedAtUtc), CommandBoundaryKey, observedAtUtc);
|
||||
AddRecord(records, schemas, ToTransportProfileDocument(observedAtUtc), TransportProfileKey, observedAtUtc);
|
||||
AddRecord(records, schemas, healthRecord, new CultRecordKey(healthRecord.DaemonId), observedAtUtc);
|
||||
|
||||
WriteSnapshot(new CultPersistedStoreSnapshot
|
||||
{
|
||||
FormatVersion = "cultcache.store.v1",
|
||||
SchemaCatalog = schemas.Values.OrderBy(entry => entry.SchemaName, StringComparer.Ordinal).ToArray(),
|
||||
Records = records.OrderBy(entry => entry.Key, StringComparer.Ordinal).ToArray(),
|
||||
});
|
||||
witnessStatus = "published";
|
||||
witnessError = string.Empty;
|
||||
witnessObservedAtUtc = observedAtUtc;
|
||||
}
|
||||
}
|
||||
catch (Exception error)
|
||||
{
|
||||
lock (publishLock)
|
||||
{
|
||||
witnessStatus = "publish-error";
|
||||
witnessError = error.Message;
|
||||
witnessObservedAtUtc = observedAtUtc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private void AddRecord<T>(
|
||||
List<CultPersistedRecord> records,
|
||||
Dictionary<string, CultSchemaCatalogEntry> schemas,
|
||||
T document,
|
||||
CultRecordKey key,
|
||||
string observedAtUtc) where T : class
|
||||
{
|
||||
var descriptor = registry.GetRequired<T>();
|
||||
var payload = MessagePackSerializer.Serialize(typeof(T), document, CultNetSchemaMessageSerialization.Options);
|
||||
schemas[descriptor.SchemaId] = descriptor.ToCatalogEntry();
|
||||
records.Add(new CultPersistedRecord
|
||||
{
|
||||
Key = key.Value,
|
||||
SchemaId = descriptor.SchemaId,
|
||||
StoredAt = observedAtUtc,
|
||||
Payload = payload,
|
||||
});
|
||||
}
|
||||
|
||||
private void WriteSnapshot(CultPersistedStoreSnapshot snapshot)
|
||||
{
|
||||
var payload = CultDocumentMessagePackSerialization.SerializeSnapshot(snapshot);
|
||||
var directory = Path.GetDirectoryName(witnessPath) ?? ".";
|
||||
Directory.CreateDirectory(directory);
|
||||
var tempPath = Path.Combine(directory, $".{Path.GetFileName(witnessPath)}.{Guid.NewGuid():N}.tmp");
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = new FileStream(
|
||||
tempPath,
|
||||
FileMode.CreateNew,
|
||||
FileAccess.Write,
|
||||
FileShare.None,
|
||||
bufferSize: 4096,
|
||||
FileOptions.WriteThrough))
|
||||
{
|
||||
stream.Write(payload, 0, payload.Length);
|
||||
stream.Flush(true);
|
||||
}
|
||||
|
||||
if (File.Exists(witnessPath))
|
||||
{
|
||||
File.Replace(tempPath, witnessPath, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(tempPath, witnessPath);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempPath))
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static EveBrowserReferenceManifestRecord ToManifestDocument(
|
||||
string documentRoot,
|
||||
int port,
|
||||
IReadOnlyList<string> assets) =>
|
||||
new()
|
||||
{
|
||||
DaemonId = "nightwing-eve-browser-reference",
|
||||
SiteTitle = "Nightwing Eve Browser Reference",
|
||||
DocumentRoot = documentRoot,
|
||||
EntryPoint = "index.html",
|
||||
Port = port,
|
||||
Assets = assets.ToArray(),
|
||||
SurfaceTransport = "static-http-lowering",
|
||||
};
|
||||
|
||||
private static EveBrowserReferenceStaticSurfaceRecord ToStaticSurfaceDocument(
|
||||
string documentRoot,
|
||||
IReadOnlyList<string> assets,
|
||||
IReadOnlyList<string> fixtures,
|
||||
string observedAtUtc) =>
|
||||
new()
|
||||
{
|
||||
DaemonId = "nightwing-eve-browser-reference",
|
||||
UpdatedAtUtc = observedAtUtc,
|
||||
DocumentRoot = documentRoot,
|
||||
EntryPoint = "index.html",
|
||||
Assets = assets.ToArray(),
|
||||
FixtureDocuments = fixtures.ToArray(),
|
||||
CurrentCutLine = "Static browser lowering assets are published into the daemon-owned witness store so Nightwing and Odin can reason about the lowering body without treating the HTTP service probe as lifecycle truth.",
|
||||
};
|
||||
|
||||
private static EveBrowserReferenceCommandBoundaryRecord ToCommandBoundaryDocument(string observedAtUtc) =>
|
||||
new()
|
||||
{
|
||||
DaemonId = "nightwing-eve-browser-reference",
|
||||
UpdatedAtUtc = observedAtUtc,
|
||||
Mode = "static-read-only-lowering",
|
||||
WritesAccepted = false,
|
||||
OperatorInputAuthority = "served browser assets only; no write path is accepted by this daemon",
|
||||
LifecycleAuthority = "idunn.local-command.restart + compatibility.systemd.nightwing-eve-browser-reference.service",
|
||||
AcceptedCommands = [],
|
||||
RejectedCommands =
|
||||
[
|
||||
"surface-mutation",
|
||||
"provider-catalog-ownership",
|
||||
"daemon-health-override",
|
||||
"service-restart",
|
||||
],
|
||||
};
|
||||
|
||||
private EveBrowserReferenceTransportProfileRecord ToTransportProfileDocument(string observedAtUtc) =>
|
||||
new()
|
||||
{
|
||||
DaemonId = "nightwing-eve-browser-reference",
|
||||
UpdatedAtUtc = observedAtUtc,
|
||||
CurrentState = "partial-rudp-health-and-provider-store-live",
|
||||
InputTransport = "compatibility.http-static-lowering",
|
||||
OutputTransport = "daemon-owned-cultcache-boundary-store + daemon-published-rudp-health",
|
||||
HealthContract = "nightwing.cultnet-rudp-browser-reference-health",
|
||||
IdunnRudpHealth = "idunn.health-published-separately",
|
||||
WitnessPath = witnessPath,
|
||||
CurrentCutLine = "The Nightwing Eve browser reference runtime now owns its manifest, static surface witness, command boundary, and transport profile in a daemon-owned CultCache store; systemd and HTTP remain compatibility witnesses while projections learn to consume the typed store directly.",
|
||||
};
|
||||
|
||||
private static string[] EnumerateRelativeFiles(string root)
|
||||
{
|
||||
if (!Directory.Exists(root))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return Directory
|
||||
.EnumerateFiles(root, "*", SearchOption.AllDirectories)
|
||||
.Select(path => Path.GetRelativePath(root, path).Replace('\\', '/'))
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
[CultDocument("mimir.eve_browser_reference_manifest", "mimir.eve_browser_reference_manifest.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class EveBrowserReferenceManifestRecord
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string DaemonId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)] public string SiteTitle { get; set; } = string.Empty;
|
||||
[Key(2)] public string DocumentRoot { get; set; } = string.Empty;
|
||||
[Key(3)] public string EntryPoint { get; set; } = string.Empty;
|
||||
[Key(4)] public int Port { get; set; }
|
||||
[Key(5)] public string[] Assets { get; set; } = [];
|
||||
[Key(6)] public string SurfaceTransport { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("mimir.eve_browser_reference_static_surface", "mimir.eve_browser_reference_static_surface.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class EveBrowserReferenceStaticSurfaceRecord
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string DaemonId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)] public string UpdatedAtUtc { get; set; } = string.Empty;
|
||||
[Key(2)] public string DocumentRoot { get; set; } = string.Empty;
|
||||
[Key(3)] public string EntryPoint { get; set; } = string.Empty;
|
||||
[Key(4)] public string[] Assets { get; set; } = [];
|
||||
[Key(5)] public string[] FixtureDocuments { get; set; } = [];
|
||||
[Key(6)] public string CurrentCutLine { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[CultDocument("mimir.eve_browser_reference_command_boundary", "mimir.eve_browser_reference_command_boundary.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class EveBrowserReferenceCommandBoundaryRecord
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string DaemonId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)] public string UpdatedAtUtc { get; set; } = string.Empty;
|
||||
[Key(2)] public string Mode { get; set; } = string.Empty;
|
||||
[Key(3)] public bool WritesAccepted { get; set; }
|
||||
[Key(4)] public string OperatorInputAuthority { get; set; } = string.Empty;
|
||||
[Key(5)] public string LifecycleAuthority { get; set; } = string.Empty;
|
||||
[Key(6)] public string[] AcceptedCommands { get; set; } = [];
|
||||
[Key(7)] public string[] RejectedCommands { get; set; } = [];
|
||||
}
|
||||
|
||||
[CultDocument("mimir.eve_browser_reference_transport_profile", "mimir.eve_browser_reference_transport_profile.v1")]
|
||||
[MessagePackObject(AllowPrivate = true)]
|
||||
internal sealed class EveBrowserReferenceTransportProfileRecord
|
||||
{
|
||||
[Key(0)]
|
||||
[CultName]
|
||||
public string DaemonId { get; set; } = string.Empty;
|
||||
|
||||
[Key(1)] public string UpdatedAtUtc { get; set; } = string.Empty;
|
||||
[Key(2)] public string CurrentState { get; set; } = string.Empty;
|
||||
[Key(3)] public string InputTransport { get; set; } = string.Empty;
|
||||
[Key(4)] public string OutputTransport { get; set; } = string.Empty;
|
||||
[Key(5)] public string HealthContract { get; set; } = string.Empty;
|
||||
[Key(6)] public string IdunnRudpHealth { get; set; } = string.Empty;
|
||||
[Key(7)] public string WitnessPath { get; set; } = string.Empty;
|
||||
[Key(8)] public string CurrentCutLine { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
internal static class IdunnRudpHealthPublisher
|
||||
{
|
||||
private const uint ConnectionId = 0x1d0d0001;
|
||||
|
||||
public static void Publish(string endpoint, IdunnDaemonHealthRecord health)
|
||||
{
|
||||
var remote = ParseEndpoint(endpoint);
|
||||
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
|
||||
socket.Bind(new IPEndPoint(IPAddress.Any, 0));
|
||||
socket.Blocking = false;
|
||||
socket.ReceiveTimeout = 100;
|
||||
socket.SendTimeout = 1000;
|
||||
using var transport = new CultNetRudpSocketTransportConnection(new CultNetRudpSocketTransportOptions
|
||||
{
|
||||
RuntimeId = "mimir-eve-browser-reference",
|
||||
Socket = socket,
|
||||
Mode = CultNetRudpSocketMode.Client,
|
||||
RemoteEndPoint = remote,
|
||||
ConnectionId = ConnectionId,
|
||||
InitialSequence = 1,
|
||||
ResendDelayMs = 100,
|
||||
});
|
||||
|
||||
if (!transport.ConnectAndWait(Array.Empty<byte>(), TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(20)))
|
||||
{
|
||||
throw new TimeoutException($"timed out connecting Mimir Eve browser reference RUDP health publisher to {endpoint}");
|
||||
}
|
||||
|
||||
var observedAt = string.IsNullOrWhiteSpace(health.ObservedAt)
|
||||
? DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)
|
||||
: health.ObservedAt;
|
||||
health.ObservedAt = observedAt;
|
||||
var message = new CultNetDocumentPutRawMessage
|
||||
{
|
||||
MessageId = $"mimir-eve-browser-reference-health:{health.DaemonId}:{observedAt.Replace(':', '-')}",
|
||||
Document = new CultNetRawDocumentRecord
|
||||
{
|
||||
SchemaId = "idunn.daemon_health",
|
||||
RecordKey = health.DaemonId,
|
||||
StoredAt = observedAt,
|
||||
PayloadEncoding = "messagepack",
|
||||
Payload = MessagePackSerializer.Serialize(health, CultNetSchemaMessageSerialization.Options),
|
||||
SourceRuntimeId = "mimir-eve-browser-reference",
|
||||
SourceRole = "daemon-health-publisher",
|
||||
Tags = ["cultnet.transport.rudp.v0"],
|
||||
},
|
||||
};
|
||||
transport.SendSchema(CultNetSchemaMessageSerialization.Serialize(message));
|
||||
}
|
||||
|
||||
private static IPEndPoint ParseEndpoint(string value)
|
||||
{
|
||||
var trimmed = value.Trim();
|
||||
var separator = trimmed.LastIndexOf(':');
|
||||
if (separator <= 0 || separator == trimmed.Length - 1)
|
||||
{
|
||||
throw new ArgumentException($"Idunn RUDP endpoint must be host:port, got '{value}'.", nameof(value));
|
||||
}
|
||||
|
||||
var host = trimmed[..separator];
|
||||
var portText = trimmed[(separator + 1)..];
|
||||
if (!int.TryParse(portText, CultureInfo.InvariantCulture, out var port))
|
||||
{
|
||||
throw new ArgumentException($"Idunn RUDP endpoint port must be numeric, got '{value}'.", nameof(value));
|
||||
}
|
||||
|
||||
var addresses = Dns.GetHostAddresses(host);
|
||||
var address = addresses.FirstOrDefault(static candidate => candidate.AddressFamily == AddressFamily.InterNetwork)
|
||||
?? throw new ArgumentException($"Idunn RUDP endpoint host did not resolve to IPv4, got '{value}'.", nameof(value));
|
||||
return new IPEndPoint(address, port);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AssemblyName>Mimir.EveDashboard</AssemblyName>
|
||||
<RootNamespace>Mimir.EveDashboard</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Networking\GameCult.Networking.csproj" />
|
||||
<ProjectReference Include="..\Mimir.Runtime\Mimir.Runtime.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -157,153 +157,6 @@ public sealed record MimirProgramOutputDocument(
|
|||
[property: Key(5)] string PublisherRoute,
|
||||
[property: Key(6)] bool DiagnosticOnly);
|
||||
|
||||
[CultDocument("mimir.eve_operator_surface", "mimir.eve_operator_surface.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveOperatorSurfaceDocument(
|
||||
[property: Key(0)]
|
||||
[property: CultName]
|
||||
string SurfaceId,
|
||||
[property: Key(1)] string UpdatedAtUtc,
|
||||
[property: Key(2)] string SceneId,
|
||||
[property: Key(3)] bool CanPreviewProgram,
|
||||
[property: Key(4)] bool CanEditComposition,
|
||||
[property: Key(5)] bool CanSelectSources,
|
||||
[property: Key(6)] bool CanShowStats,
|
||||
[property: Key(7)] string[] CommandTopics);
|
||||
|
||||
[CultDocument("mimir.eve_dashboard_manifest", "mimir.eve_dashboard_manifest.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardManifestDocument(
|
||||
[property: Key(0)]
|
||||
[property: CultName]
|
||||
string ProviderId,
|
||||
[property: Key(1)] string Title,
|
||||
[property: Key(2)] string Description,
|
||||
[property: Key(3)] string Version,
|
||||
[property: Key(4)] string Endpoint,
|
||||
[property: Key(5)] string[] Capabilities,
|
||||
[property: Key(6)] bool UsesCultMesh,
|
||||
[property: Key(7)] string Transport);
|
||||
|
||||
[CultDocument("mimir.eve_dashboard_state", "mimir.eve_dashboard_state.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardStateDocument(
|
||||
[property: Key(0)]
|
||||
[property: CultName]
|
||||
string ProviderId,
|
||||
[property: Key(1)] string Title,
|
||||
[property: Key(2)] long Version,
|
||||
[property: Key(3)] string UpdatedAtUtc,
|
||||
[property: Key(4)] string SelectedNodeId,
|
||||
[property: Key(5)] string LutPreset,
|
||||
[property: Key(6)] MimirEveDashboardNodeSnapshot[] Nodes,
|
||||
[property: Key(7)] MimirEveDashboardSurfaceSnapshot? Surface);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardNodeSnapshot(
|
||||
[property: Key(0)] string Id,
|
||||
[property: Key(1)] string Label,
|
||||
[property: Key(2)] string Kind,
|
||||
[property: Key(3)] bool Visible,
|
||||
[property: Key(4)] double X,
|
||||
[property: Key(5)] double Y,
|
||||
[property: Key(6)] double Z,
|
||||
[property: Key(7)] double Rotation,
|
||||
[property: Key(8)] double Scale,
|
||||
[property: Key(9)] double Width,
|
||||
[property: Key(10)] double Height,
|
||||
[property: Key(11)] string Health,
|
||||
[property: Key(12)] string? ProviderId,
|
||||
[property: Key(13)] string? Command,
|
||||
[property: Key(14)] string? Endpoint);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardSurfaceSnapshot(
|
||||
[property: Key(0)] string Schema,
|
||||
[property: Key(1)] string Id,
|
||||
[property: Key(2)] string Title,
|
||||
[property: Key(3)] MimirEveDashboardUiElementSnapshot Root,
|
||||
[property: Key(4)] MimirEveDashboardSurfaceAssetSnapshot[] Assets);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardSurfaceAssetSnapshot(
|
||||
[property: Key(0)] string Id,
|
||||
[property: Key(1)] string Kind,
|
||||
[property: Key(2)] string Uri);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardUiElementSnapshot(
|
||||
[property: Key(0)] string Id,
|
||||
[property: Key(1)] string Kind,
|
||||
[property: Key(2)] string? Role,
|
||||
[property: Key(3)] string? Text,
|
||||
[property: Key(4)] string? AssetRef,
|
||||
[property: Key(5)] string? AssetUri,
|
||||
[property: Key(6)] string? BindNodeId,
|
||||
[property: Key(7)] string? CommandId,
|
||||
[property: Key(8)] MimirEveDashboardUiLayoutSnapshot? Layout,
|
||||
[property: Key(9)] MimirEveDashboardUiStyleSnapshot? Style,
|
||||
[property: Key(10)] MimirEveDashboardUiMetricSnapshot? Metric,
|
||||
[property: Key(11)] MimirEveDashboardUiElementSnapshot[] Children,
|
||||
[property: Key(12)] MimirEveDashboardUiBindingSnapshot? Binding = null);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardUiBindingSnapshot(
|
||||
[property: Key(0)] string DocumentSchema,
|
||||
[property: Key(1)] string DocumentId,
|
||||
[property: Key(2)] string Path,
|
||||
[property: Key(3)] string ValueKind,
|
||||
[property: Key(4)] string Access,
|
||||
[property: Key(5)] string Authority,
|
||||
[property: Key(6)] string? CommandId);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardUiLayoutSnapshot(
|
||||
[property: Key(0)] string Direction,
|
||||
[property: Key(1)] double? Width,
|
||||
[property: Key(2)] double? Height,
|
||||
[property: Key(3)] double? Grow,
|
||||
[property: Key(4)] double? Gap,
|
||||
[property: Key(5)] double? Padding,
|
||||
[property: Key(6)] string? Overflow,
|
||||
[property: Key(7)] double? MinWidth = null,
|
||||
[property: Key(8)] double? MinHeight = null,
|
||||
[property: Key(9)] double? PreferredWidth = null,
|
||||
[property: Key(10)] double? PreferredHeight = null,
|
||||
[property: Key(11)] double? Priority = null,
|
||||
[property: Key(12)] string? Density = null,
|
||||
[property: Key(13)] string? ViewportMode = null);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardUiStyleSnapshot(
|
||||
[property: Key(0)] string Variant,
|
||||
[property: Key(1)] string? Tone);
|
||||
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardUiMetricSnapshot(
|
||||
[property: Key(0)] string Label,
|
||||
[property: Key(1)] double Value,
|
||||
[property: Key(2)] string Tone);
|
||||
|
||||
[CultDocument("mimir.eve_dashboard_command", "mimir.eve_dashboard_command.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveDashboardCommandDocument(
|
||||
[property: Key(0)]
|
||||
[property: CultName]
|
||||
string CommandId,
|
||||
[property: Key(1)] string DeviceId,
|
||||
[property: Key(2)] string ClientId,
|
||||
[property: Key(3)] string ProviderId,
|
||||
[property: Key(4)] string Type,
|
||||
[property: Key(5)] string NodeId,
|
||||
[property: Key(6)] double? X,
|
||||
[property: Key(7)] double? Y,
|
||||
[property: Key(8)] double? Rotation,
|
||||
[property: Key(9)] double? Scale,
|
||||
[property: Key(10)] bool? Visible,
|
||||
[property: Key(11)] long Sequence,
|
||||
[property: Key(12)] long DeviceTimestampNs);
|
||||
|
||||
[CultDocument("mimir.eve_sensor_observation", "mimir.eve_sensor_observation.v1")]
|
||||
[MessagePackObject]
|
||||
public sealed record MimirEveSensorObservationDocument(
|
||||
|
|
@ -482,29 +335,6 @@ public static class MimirCultMeshContractFactory
|
|||
publication.SitePublisherRoute,
|
||||
publication.DiagnosticOnly);
|
||||
|
||||
public static MimirEveOperatorSurfaceDocument CreateOperatorSurface(
|
||||
string surfaceId,
|
||||
string sceneId,
|
||||
MimirProgramControlSurface surface,
|
||||
DateTimeOffset? updatedAt = null) =>
|
||||
new(
|
||||
surfaceId,
|
||||
(updatedAt ?? DateTimeOffset.UtcNow).ToString("O"),
|
||||
sceneId,
|
||||
surface.CanPreviewProgram,
|
||||
surface.CanEditComposition,
|
||||
surface.CanSelectSources,
|
||||
surface.CanShowStats,
|
||||
CommandTopics:
|
||||
[
|
||||
"mimir.program.scene.select",
|
||||
"mimir.program.layer.transform",
|
||||
"mimir.program.layer.crop",
|
||||
"mimir.program.layer.chroma_key",
|
||||
"mimir.program.source.subscribe",
|
||||
"mimir.program.output.publish"
|
||||
]);
|
||||
|
||||
private static MimirBioacousticDecoderConfigurationSnapshot Snapshot(MimirBioacousticDecoderConfiguration configuration) =>
|
||||
new(
|
||||
configuration.Id,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ public enum MimirProgramAudioPublicationKind
|
|||
|
||||
public enum MimirProgramControlPublicationKind
|
||||
{
|
||||
EveGui,
|
||||
EveTui,
|
||||
CultMeshCommandSurface
|
||||
}
|
||||
|
||||
|
|
@ -70,25 +68,9 @@ public static class MimirProgramPublicationConfigurations
|
|||
|
||||
public static IReadOnlyList<MimirProgramControlSurface> OperatorSurfaces { get; } =
|
||||
[
|
||||
new(
|
||||
"eve-gui-compositor",
|
||||
"Primary Eve GUI composition surface: preview, source selection, transforms, crop/key controls, audio stem controls, and stream health.",
|
||||
MimirProgramControlPublicationKind.EveGui,
|
||||
CanPreviewProgram: true,
|
||||
CanEditComposition: true,
|
||||
CanSelectSources: true,
|
||||
CanShowStats: true),
|
||||
new(
|
||||
"eve-tui-operator",
|
||||
"Compact Eve TUI operator surface for source health, live stats, scene selection, and emergency command paths.",
|
||||
MimirProgramControlPublicationKind.EveTui,
|
||||
CanPreviewProgram: false,
|
||||
CanEditComposition: false,
|
||||
CanSelectSources: true,
|
||||
CanShowStats: true),
|
||||
new(
|
||||
"cultmesh-command-surface",
|
||||
"Typed CultMesh command surface consumed by Eve lowerers and trusted devices; Mimir remains the scene authority.",
|
||||
"Typed CultMesh command surface consumed by trusted devices; Mimir remains the scene authority.",
|
||||
MimirProgramControlPublicationKind.CultMeshCommandSurface,
|
||||
CanPreviewProgram: false,
|
||||
CanEditComposition: true,
|
||||
|
|
|
|||
|
|
@ -652,16 +652,6 @@ accepted_design:
|
|||
receiver. The old `CultMesh.StartNodeAsync`/`CultMesh.ConnectClient`
|
||||
node/client path and raw sender/receiver host-port config are no longer
|
||||
transport authorities for this media lane.
|
||||
- `src/Mimir.EveDashboard` is archived as a fail-closed tombstone. It no
|
||||
longer opens a TCP listener, serves `/eve/deck`, accepts WebSocket commands,
|
||||
or publishes Idunn health. Its replacement must publish provider
|
||||
advertisement, retained state, command boundary, and transport profile as
|
||||
typed CultMesh/Odin documents; product/debug render surfaces remain client
|
||||
lowerings.
|
||||
- `src/Mimir.EveBrowserReference` serves a static Eve/browser lowering and
|
||||
can publish its own `idunn.daemon_health` record over
|
||||
`cultnet.transport.rudp.v0`. It is a renderer/reference health witness, not
|
||||
an authority over Mimir program state or Idunn keepalive decisions.
|
||||
- `research/perfect-machine-study-2026-05-23/` preserves the architecture
|
||||
rumination, optimization ledger, research references, source/hotspot audits,
|
||||
Fensalir/native boundary maps, benchmark plan, calibration-session spec,
|
||||
|
|
@ -690,13 +680,7 @@ active_subgoals:
|
|||
replaces them.
|
||||
- Launch Raven's CultMesh sender directly on Raven because SSH to Raven over
|
||||
WireGuard timed out during the initial relay deployment pass.
|
||||
- Keep Mimir Eve dashboard and browser reference aligned with Odin/Idunn
|
||||
doctrine: Odin accepts Verse/service visibility, Idunn owns keepalive, and
|
||||
Mimir surfaces publish typed CultMesh/Odin state rather than private
|
||||
watchdog loops, product status probes, or socket brokers.
|
||||
- Rebuild `src/Mimir.EveDashboard` only after daemon truth, provider catalogs,
|
||||
command boundaries, and typed program document paths use explicit CultMesh/
|
||||
Odin documents. `Mimir.EveSensorReceiver`, `Mimir.VerseRecorder`, the
|
||||
- `Mimir.EveSensorReceiver`, `Mimir.VerseRecorder`, the
|
||||
Nightwing typed witness publisher, and `start-nightwing-move-tracking.ps1`
|
||||
are archived so the old witness subscription lane cannot retake transport
|
||||
authority. `src/Mimir.CultMeshMedia` has completed this cut for the
|
||||
|
|
|
|||
Loading…
Reference in a new issue