Serve Odin snapshots and retire Gjallar bridge debt

This commit is contained in:
Meta Vens 2026-06-17 01:28:55 +01:00
commit b43d2a3136
7 changed files with 322 additions and 89 deletions

View file

@ -21,16 +21,17 @@ documents, ingest ports, normalization, and repository persistence so unit tests
can use mocked inputs and pipeline smokes can prove typed handoff without
booting the whole daemon. Gjallar is not part of that Rust record spine: it is
the Nightwing-resident terminal compositor in `E:\Projects\Gjallar` that
consumes Odin's published Eve/CultUI deck and renders the live display.
consumes Odin's accepted `gamecult.eve.surface_state` snapshot over CultNet/RUDP
and renders the live display.
## Gjallar
Gjallar is the herald display daemon that runs on Nightwing. Odin sees the
Verses, accepts provider surfaces, and publishes the `odin.providers` catalog.
Gjallar talks to Odin, enumerates active providers, composes the multi-scale
tiled dashboard, lowers Odin's canonical marquee tape into continuous gutter
text, owns dense character-level update behavior, and writes the visible
framebuffer.
Gjallar consumes Odin's accepted surface snapshot over CultNet/RUDP, composes
the multi-scale tiled dashboard from that typed state, lowers Odin's canonical
marquee tape into continuous gutter text, owns dense character-level update
behavior, and writes the visible framebuffer.
Local package surfaces:
@ -95,7 +96,13 @@ The Eve/CultUI deck endpoint is a compatibility bridge:
ws://127.0.0.1:8797/eve/deck
```
Nightwing can consume the LAN bridge endpoint:
The native CultNet/RUDP snapshot endpoint is:
```text
127.0.0.1:17871
```
Legacy browser/deck lowerers can consume the LAN bridge endpoint:
```text
ws://192.168.1.66:8797/eve/deck

View file

@ -951,14 +951,7 @@ fn swarm_surgery_plan(
plans: &[IdunnDaemonSurgeryPlanRecord],
updated_at: &str,
) -> IdunnSwarmSurgeryPlanRecord {
let next_target = preferred_next_target(targets, plans);
let next_target = if let Some(target) = next_target {
target
} else if let Some(target) = targets.iter().find(|target| target.enabled) {
target.daemon_id.as_str()
} else {
"none"
};
let next_target = preferred_next_target(targets, plans).unwrap_or("none");
IdunnSwarmSurgeryPlanRecord {
plan_id: format!("swarm-surgery:{profile}"),
@ -987,11 +980,11 @@ fn swarm_surgery_plan(
"5. Delete or demote compatibility probes once every target has daemon-owned publication and advertised lifecycle authority.".to_string(),
],
current_phase:
"Phase 24: Nightwing Gjallar now publishes a daemon-owned gamecult.eve.surface_state witness, Odin proxies a real Nightwing Gjallar panel from the live remote store, and the remaining Gjallar debt is replacing the compatibility Odin WebSocket deck input with native CultMesh/CultNet intake."
"Phase 25: Nightwing Gjallar now consumes Odin's accepted gamecult.eve.surface_state snapshot over CultNet/RUDP, so every active Starfire-local daemon target publishes daemon-owned RUDP health and typed boundary state; the remaining debt is demoting compatibility probes and bridge-only lowerings without letting them retake ownership."
.to_string(),
next_target: next_target.to_string(),
cut_line:
"Muninn, Vili, Idunn, Odin, Stonks, Weksa, VoidBot, Nightwing Gjallar, Mimir Eve dashboard, Nightwing Eve dashboard, Nightwing Eve browser reference, yggdrasil-streampixels, yggdrasil-heimdall, and yggdrasil-repixelizer now exercise daemon-owned RUDP health. Raven GameCult\\Vili has been refreshed from Odin, the hidden scheduled task now launches start-vili-daemon.ps1 with Idunn RUDP arguments, and live Idunn accepts vili.cultnet-rudp-animation-health from 10.77.0.4. VoidBot now publishes a daemon-owned provider catalog, provider advertisements, command_boundary, and transport_profile records from E:\\Projects\\VoidBot\\.voidbot\\status\\cultmesh\\voidbot-swarm-state.cc while health-voidbot.cmd is demoted to a fast compatibility witness. Raven Muninn now publishes provider advertisement, command_boundary, and transport_profile records from C:\\Meta\\Odin\\state\\muninn.telemetry.cc, Starfire Muninn now publishes the same daemon-owned witness records from C:\\Meta\\Odin\\state\\starfire.muninn.telemetry.cc, Nightwing Muninn now publishes the same daemon-owned witness records from /home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc, and live Idunn accepts muninn from 10.77.0.4, starfire-muninn from 127.0.0.1, and nightwing-muninn from 10.77.0.3. Raven's GameCult-Muninn, GameCult-Muninn-Activate, and GameCult-Muninn-VideoProof tasks now execute wscript.exe actions whose hidden VBS launchers call noninteractive hidden PowerShell entrypoints directly; .cmd wrappers, where present, are manual compatibility entrypoints only. Live yggdrasil-streampixels publishes streampixels.cultnet-rudp-service-health from 10.77.0.1, live yggdrasil-heimdall publishes heimdall.cultnet-rudp-provider-health from 10.77.0.1 with a boundary store at /srv/heimdall/cultcache/heimdall.service.cc, live yggdrasil-repixelizer publishes repixelizer.cultnet-rudp-service-health from 10.77.0.1 with a boundary store at /srv/repixelizer/cultcache/repixelizer.service.cc, live Nightwing Gjallar publishes a boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc plus a daemon-owned gamecult.eve.surface_state witness, live Odin now ingests remote Raven Vili and Nightwing Gjallar witness stores into the accepted provider catalog, and /eve/deck/nightwing-gjallar now proxies a real Nightwing Gjallar panel from the live remote store. Live Mimir Eve dashboard now publishes CultMesh state at /var/lib/gamecult/eve-dashboard/cultmesh/eve-dashboard.ccmp plus a boundary store at /var/lib/gamecult/eve-dashboard/cultcache/eve-dashboard.service.cc, and live Nightwing Eve browser reference now publishes a boundary store at /var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc. Gjallar remains the next compatibility debt until its native CultMesh/CultNet input path replaces the Odin WebSocket deck bridge."
"Muninn, Vili, Idunn, Odin, Stonks, Weksa, VoidBot, Nightwing Gjallar, Mimir Eve dashboard, Nightwing Eve dashboard, Nightwing Eve browser reference, yggdrasil-streampixels, yggdrasil-heimdall, and yggdrasil-repixelizer now exercise daemon-owned RUDP health. Raven GameCult\\Vili has been refreshed from Odin, the hidden scheduled task now launches start-vili-daemon.ps1 with Idunn RUDP arguments, and live Idunn accepts vili.cultnet-rudp-animation-health from 10.77.0.4. VoidBot now publishes a daemon-owned provider catalog, provider advertisements, command_boundary, and transport_profile records from E:\\Projects\\VoidBot\\.voidbot\\status\\cultmesh\\voidbot-swarm-state.cc while health-voidbot.cmd is demoted to a fast compatibility witness. Raven Muninn now publishes provider advertisement, command_boundary, and transport_profile records from C:\\Meta\\Odin\\state\\muninn.telemetry.cc, Starfire Muninn now publishes the same daemon-owned witness records from C:\\Meta\\Odin\\state\\starfire.muninn.telemetry.cc, Nightwing Muninn now publishes the same daemon-owned witness records from /home/metacrat/.local/state/gamecult/muninn/muninn.telemetry.cc, and live Idunn accepts muninn from 10.77.0.4, starfire-muninn from 127.0.0.1, and nightwing-muninn from 10.77.0.3. Raven's GameCult-Muninn, GameCult-Muninn-Activate, and GameCult-Muninn-VideoProof tasks now execute wscript.exe actions whose hidden VBS launchers call noninteractive hidden PowerShell entrypoints directly; .cmd wrappers, where present, are manual compatibility entrypoints only. Live yggdrasil-streampixels publishes streampixels.cultnet-rudp-service-health from 10.77.0.1, live yggdrasil-heimdall publishes heimdall.cultnet-rudp-provider-health from 10.77.0.1 with a boundary store at /srv/heimdall/cultcache/heimdall.service.cc, live yggdrasil-repixelizer publishes repixelizer.cultnet-rudp-service-health from 10.77.0.1 with a boundary store at /srv/repixelizer/cultcache/repixelizer.service.cc, live Nightwing Gjallar publishes a boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc plus a daemon-owned gamecult.eve.surface_state witness, live Gjallar now consumes Odin's accepted surface:gamecult.network.status snapshot over CultNet/RUDP from 192.168.1.66:17871, live Odin now ingests remote Raven Vili and Nightwing Gjallar witness stores into the accepted provider catalog, and /eve/deck/nightwing-gjallar now proxies a renderer-only lowering of the live remote store. Live Mimir Eve dashboard now publishes CultMesh state at /var/lib/gamecult/eve-dashboard/cultmesh/eve-dashboard.ccmp plus a boundary store at /var/lib/gamecult/eve-dashboard/cultcache/eve-dashboard.service.cc, and live Nightwing Eve browser reference now publishes a boundary store at /var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc. The remaining debt is demoting compatibility probes and bridge-only lowerings without letting them retake daemon truth."
.to_string(),
verification_layer:
"CultMesh keepalive store records plus live Idunn decision cycles, not process exit codes or chat summaries."
@ -1238,9 +1231,9 @@ fn daemon_transport_profile(
"VoidBot stack health is published over CultNet/RUDP from the local orchestrator pulse, and the daemon-owned CultMesh witness store at E:\\Projects\\VoidBot\\.voidbot\\status\\cultmesh\\voidbot-swarm-state.cc now carries provider advertisement catalog, provider advertisement, command_boundary, and transport_profile records. The operations probe remains fallback evidence only.",
),
"nightwing-gjallar" => (
"daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + compatibility.local-command fallback",
"daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + native-odin-cultnet-rudp-snapshot-input + compatibility.local-command fallback",
"partial-rudp-health-and-provider-store-live",
"Gjallar framebuffer composition health is published over CultNet/RUDP from Nightwing, and the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc carries provider advertisement, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary state. Native C# CultNet/RUDP deck input remains migration debt before the service/status probe can be deleted.",
"Gjallar framebuffer composition health is published over CultNet/RUDP from Nightwing, the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc carries provider advertisement, runtime_config, frame_status, gamecult.eve.surface_state, command_boundary, transport_profile, and daemon-health summary state, and live composition now consumes Odin's accepted surface:gamecult.network.status snapshot over CultNet/RUDP. The service/status probe is fallback evidence only.",
),
"nightwing-eve-dashboard" => (
"daemon-published-rudp-health + daemon-owned-cultmesh-state + daemon-owned-cultcache-boundary-store + compatibility.local-command fallback",
@ -1401,23 +1394,23 @@ fn daemon_surgery_plan(target: &DaemonTarget, updated_at: &str) -> IdunnDaemonSu
.to_string();
}
"nightwing-gjallar" => {
status = "catalog-coherence-probe";
status = "partial-rudp-health-and-provider-store-live";
severity = "critical";
owner = "Gjallar C# runtime plus Odin provider feed";
owner = "Gjallar C# runtime plus Odin accepted snapshot surface";
current_mechanism =
"Nightwing Gjallar publishes gjallar.cultnet-rudp-framebuffer-composition-health over CultNet/RUDP from the C# runtime, and the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc now contains provider advertisement, gamecult.eve.surface_state, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary records. Live Odin ingests that witness store and now proxies /eve/deck/nightwing-gjallar as a real panel."
"Nightwing Gjallar publishes gjallar.cultnet-rudp-framebuffer-composition-health over CultNet/RUDP from the C# runtime, the daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc now contains provider advertisement, gamecult.eve.surface_state, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary records, and the live compositor now polls Odin's accepted surface:gamecult.network.status snapshot over cultnet.transport.rudp.v0 from 192.168.1.66:17871. Live Odin ingests that witness store and /eve/deck/nightwing-gjallar remains a renderer-only lowering."
.to_string();
intended_authority =
"Gjallar subscribes to Odin/provider deck state over CultNet/RUDP and publishes framebuffer composition health, provider advertisement, provider-owned Eve surface, runtime config, command boundary, and transport profile as typed CultMesh/CultNet state."
"Gjallar consumes Odin's accepted provider surface snapshot over CultNet/RUDP and publishes framebuffer composition health, provider advertisement, provider-owned Eve surface, runtime config, command boundary, and transport profile as typed CultMesh/CultNet state."
.to_string();
cut_line =
"Keep the service/status probe as fallback only until Gjallar's native CultMesh/RUDP input path replaces the Odin WebSocket deck bridge; the daemon-owned witness now owns provider advertisement, gamecult.eve.surface_state, command_boundary, and transport_profile truth, and the remaining debt is deleting the compatibility deck bridge from the live input path."
"Keep the service/status probe as fallback only; the daemon-owned witness now owns provider advertisement, gamecult.eve.surface_state, command_boundary, and transport_profile truth, live input now comes from Odin's accepted CultNet/RUDP snapshot surface, and the remaining debt is deleting or demoting compatibility probes and renderer-only deck lowerings without letting them reclaim daemon truth."
.to_string();
steps = vec![
"Keep live gjallar.cultnet-rudp-framebuffer-composition-health publication running from Nightwing's Gjallar service.".to_string(),
"Keep Gjallar's daemon-owned boundary store at /var/lib/gamecult/gjallar/cultcache/gjallar.service.cc publishing provider advertisement, gamecult.eve.surface_state, runtime_config, frame_status, command_boundary, transport_profile, and daemon-health summary records.".to_string(),
"Replace Odin WebSocket deck consumption with a native C# CultMesh/RUDP input path when the deck contract is ready.".to_string(),
"Keep Odin's hashed-store ingestion shim strictly as a xenos-boundary bridge until Gjallar's native CultMesh/CultNet input path and interoperable witness path make the compatibility deck bridge disposable.".to_string(),
"Keep the native C# CultNet/RUDP snapshot polling path pointed at Odin's accepted surface:gamecult.network.status surface on 192.168.1.66:17871.".to_string(),
"Keep /eve/deck/nightwing-gjallar as a renderer-only lowering; it must not become Gjallar's live input path again.".to_string(),
"Delete or demote health-nightwing-gjallar.cmd to a manual deployment witness with no lifecycle truth.".to_string(),
];
}
@ -2829,9 +2822,9 @@ mod tests {
assert_eq!(plan.plan_id, "swarm-surgery:starfire-local");
assert_eq!(plan.status, "active-transport-migration");
assert_eq!(plan.next_target, "nightwing-gjallar");
assert_eq!(plan.next_target, "none");
assert!(plan.current_phase.contains(
"Nightwing Gjallar now publishes a daemon-owned gamecult.eve.surface_state witness"
"Nightwing Gjallar now consumes Odin's accepted gamecult.eve.surface_state snapshot over CultNet/RUDP"
));
assert!(
plan.cut_line.contains(
@ -2904,10 +2897,9 @@ mod tests {
assert!(plan.cut_line.contains(
"/var/lib/gamecult/eve-browser-reference/cultcache/eve-browser-reference.service.cc"
));
assert!(
plan.cut_line
.contains("Gjallar remains the next compatibility debt")
);
assert!(plan.cut_line.contains("surface:gamecult.network.status"));
assert!(plan.cut_line.contains("192.168.1.66:17871"));
assert!(plan.cut_line.contains("renderer-only lowering"));
assert!(plan.verification_layer.contains("CultMesh keepalive store"));
assert!(
plan.invariants
@ -3033,13 +3025,21 @@ mod tests {
);
let gjallar_plan = daemon_surgery_plan(&nightwing_gjallar, "unix:100");
assert_eq!(gjallar_plan.status, "catalog-coherence-probe");
assert_eq!(
gjallar_plan.status,
"partial-rudp-health-and-provider-store-live"
);
assert!(
gjallar_plan
.current_mechanism
.contains("gjallar.cultnet-rudp-framebuffer-composition-health")
);
assert!(gjallar_plan.cut_line.contains("gamecult.eve.surface_state"));
assert!(
gjallar_plan
.current_mechanism
.contains("surface:gamecult.network.status")
);
let nightwing_eve_dashboard_plan =
daemon_surgery_plan(&nightwing_eve_dashboard, "unix:100");
@ -3423,13 +3423,14 @@ mod tests {
assert_eq!(profile.state, "partial-rudp-health-and-provider-store-live");
assert_eq!(
profile.current_transport,
"daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + compatibility.local-command fallback"
"daemon-published-rudp-health + daemon-owned-cultcache-service-boundary + native-odin-cultnet-rudp-snapshot-input + compatibility.local-command fallback"
);
assert!(
profile
.cut_line
.contains("Gjallar framebuffer composition health is published over CultNet/RUDP")
);
assert!(profile.cut_line.contains("surface:gamecult.network.status"));
}
#[test]

View file

@ -1,6 +1,7 @@
param(
[int] $Port = 8797,
[string] $StateDir = "E:\Projects\Odin\scratch\odin",
[string] $CultNetRudpBind = "0.0.0.0:17871",
[string] $IdunnRudpHealth = "127.0.0.1:17870",
[string] $IdunnDaemon = "odin",
[string] $IdunnHealthContract = "odin.cultnet-rudp-provider-health",
@ -33,6 +34,7 @@ $args = @(
$scriptPath,
"--port", "$Port",
"--stateDir", $StateDir,
"--cultnet-rudp-bind", $CultNetRudpBind,
"--idunn-rudp-health", $IdunnRudpHealth,
"--idunn-daemon", $IdunnDaemon,
"--idunn-health-contract", $IdunnHealthContract
@ -56,3 +58,4 @@ if ($proc.HasExited) {
Write-Host "Odin started as PID $($proc.Id)."
Write-Host "Health: http://127.0.0.1:$Port/health"
Write-Host "Eve deck: ws://127.0.0.1:$Port/eve/deck"
Write-Host "CultNet/RUDP snapshot: $CultNetRudpBind"

View file

@ -56,9 +56,11 @@ Idunn is a Rust daemon inside Odin's Cargo workspace. The live local runtime is
one long-lived `idunn.exe` process that owns the whole Starfire-local swarm:
Odin, local adjunct daemons, the Yggdrasil deploy lanes, and the Nightwing
display services. Each target declares a daemon-owned health contract and keeps
its own interval and deploy/restart authority. The current shell health
commands are compatibility probes until each daemon updates its CultLib
dependency and publishes health through CultNet over the shared RUDP transport.
its own interval and deploy/restart authority. The shell health commands now
exist as compatibility probes and actuators; daemon truth is expected to come
first from typed CultNet/RUDP publication and daemon-owned boundary stores, and
the local commands only matter when that witness is absent or when an explicit
restart/deploy boundary is advertised.
The scheduler and continuity witness now belong to one Rust process instead of
a PowerShell-herded pile of one-daemon workers.
@ -220,61 +222,33 @@ probe. The record must match the daemon id, health contract, RUDP transport,
and freshness window; otherwise the compatibility probe remains fallback
evidence.
The current active cut has moved past Raven daemon launcher surgery and into the
remaining Yggdrasil deployments, with repixelizer now the explicit next target.
Heimdall has crossed the first live transport cut: the Yggdrasil runtime now
publishes `heimdall.cultnet-rudp-provider-health`, writes a daemon-owned
boundary witness at `/srv/heimdall/cultcache/heimdall.service.cc`, and ships
the required `CultLib` snapshot through the source-artifact lane. Heimdall
still owes Odin ingestion of that typed store plus the later redacted
auth-document witness export, but it is no longer blocked on basic daemon
health publication. Vili now publishes RUDP health from Raven
through the hidden `GameCult\Vili` scheduled task, and live Idunn accepts
`vili.cultnet-rudp-animation-health` from `10.77.0.4`. The Raven repair
actuator in [restart-vili.ps1](/E:/Projects/Odin/scripts/restart-vili.ps1)
syncs the authoritative Vili runtime plus flattened CultLib node modules before
reinstalling the hidden task. Vili also writes a daemon-owned
`.vili\vili.service.cc` store with provider advertisement, operator state, Eve
surface, command boundary, and transport profile records; Odin local discovery
can ingest that typed store. Weksa publishes RUDP health and a daemon-owned
provider store containing provider advertisement, operator state, Eve surface,
command boundary, and transport profile records; Odin local discovery can ingest
that typed store too. Weksa still owes CultNet/RUDP command document ingress
for MiMo VoiceDesign before its HTTP command route can become debug-only.
Stonks publishes RUDP health and a daemon-owned CultCache store containing
provider advertisement, market snapshot, Eve surface, command boundary, and
transport profile records; Odin local discovery can ingest that store as
provider truth. StreamPixels now publishes a service-owned CultCache boundary
store with provider advertisement, command boundary, transport profile, and
Idunn health summary; Odin local discovery can ingest that store, and the live
Yggdrasil service now publishes daemon health over CultNet/RUDP with
`STREAMPIXELS_IDUNN_RUDP_HEALTH=10.77.0.2:17870` and contract
`streampixels.cultnet-rudp-service-health` in
`/srv/streampixels/env/service.env`. Live Idunn accepts
`yggdrasil-streampixels` from `10.77.0.1`, and the service-owned boundary store
now lives at
`/srv/streampixels/app/.streampixels-data/cultcache/streampixels.service.cc`.
The remaining debt is only to demote SSH/systemd/HTTP probes from compatibility
proof to deployment/debug witness once Odin consumes the typed store without
fallback. Repixelizer is still plain GUI/systemd compatibility debt for now,
which makes it the next Yggdrasil surgery target:
`repixelizer-gui.service`, `/api/health`, `/api/config`, and nginx routing are
still the live witnesses until the runtime publishes internal RUDP health and
typed queue/provider state. Raven Muninn task actions are also an explicit ops invariant: Task
Scheduler must execute `wscript.exe //B //Nologo` hidden launcher actions
directly for `GameCult-Muninn`, `GameCult-Muninn-Activate`, and
The active transport cut is no longer "make the daemons speak RUDP at all."
Nightwing Gjallar now consumes Odin's accepted
`surface:gamecult.network.status` snapshot over CultNet/RUDP, so every active
Starfire-local daemon target now publishes daemon-owned RUDP health plus typed
boundary state. The remaining debt is deleting or demoting compatibility
probes, HTTP/WebSocket/TCP lowerings, and xenos-boundary ingestion shims
without letting them reclaim ownership. Weksa still owes CultNet/RUDP command
document ingress for MiMo VoiceDesign before its HTTP command route can become
debug-only, and Odin still owes removal of hashed-store/C# witness shims once
direct interoperable witness publication is ready.
Raven Muninn task actions are also an explicit ops invariant: Task Scheduler
must execute `wscript.exe //B //Nologo` hidden launcher actions directly for
`GameCult-Muninn`, `GameCult-Muninn-Activate`, and
`GameCult-Muninn-VideoProof`, never raw `.cmd` wrappers, and the hidden VBS
layer must call `.ps1` launchers directly instead of `cmdPath` trampolines.
Those three live Raven tasks have been repaired and verified. The repair actuator now uploads its
Raven PowerShell body with `sftp` and runs a tiny cleanup wrapper, so the
hidden-task repair does not hit Windows command-line limits before it can run.
The long-running Muninn serve bodies on Raven, Nightwing, and Starfire now also
carry their own `--idunn-rudp-health`, `--idunn-daemon`, and
`--idunn-health-contract` arguments, and live Idunn accepts those daemon-owned
health records directly instead of relying on one-shot health commands for the
truth.
After those live cuts, continue daemon-by-daemon until compatibility HTTP, TCP,
WebSocket, and command probes are fallback witnesses only.
Those three live Raven tasks have been repaired and verified. The repair
actuator now uploads its Raven PowerShell body with `sftp` and runs a tiny
cleanup wrapper, so the hidden-task repair does not hit Windows command-line
limits before it can run. The long-running Muninn serve bodies on Raven,
Nightwing, and Starfire now also carry their own `--idunn-rudp-health`,
`--idunn-daemon`, and `--idunn-health-contract` arguments, and live Idunn
accepts those daemon-owned health records directly instead of relying on
one-shot health commands for the truth.
From here on out, the work is not "teach another daemon to speak." It is
"delete the witness masks and keep ownership where it belongs."
Rust no longer gets to claim the transport is imaginary:
`vendor/cultnet-rs` includes the canonical CNR0 `cultnet.transport.rudp.v0`

View file

@ -5,6 +5,7 @@ const fs = require("fs");
const path = require("path");
const { buildConfig, loadCultRuntime } = require("./odin/config.cjs");
const { createCultNetRudpSurfaceServer } = require("./odin/cultnet-rudp.cjs");
const { defineOdinDocuments } = require("./odin/documents.cjs");
const { createInterfaceDiscovery } = require("./odin/interfaces.cjs");
const { createIdunnRudpHealthPublisher, publishIdunnRudpHealth } = require("./odin/idunn-rudp.cjs");
@ -41,6 +42,7 @@ const stateBuilder = createStateBuilder({
});
let meshNodePromise = null;
let cultNetRudpSurfaceServer = null;
let currentState = stateBuilder.buildPendingState("Coordinator starting");
let lastRefresh = {
completedAt: null,
@ -59,6 +61,20 @@ async function main() {
if (cultRuntime.CultMesh && documents.surfaceDefinition) {
meshNodePromise = createDurableSurfaceNode();
}
if (config.cultnetRudpBind) {
cultNetRudpSurfaceServer = createCultNetRudpSurfaceServer({
bind: config.cultnetRudpBind,
documents,
getCache: async () => {
if (!meshNodePromise) {
throw new Error("CultMesh runtime is unavailable; Odin cannot serve a CultNet/RUDP snapshot.");
}
return (await meshNodePromise).cache;
},
});
await cultNetRudpSurfaceServer.start();
console.log(`CultNet/RUDP snapshot: ${config.cultnetRudpBind}`);
}
const dashboardServer = createDashboardServer({
applyClientCommand: (command) => layoutStore.applyClientCommand(command),
@ -177,6 +193,7 @@ function health(clients) {
error: cultRuntime.error?.message || null,
},
discovery: {
cultNetRudpBind: config.cultnetRudpBind || null,
seedDeckUrls: config.seedDeckUrls,
discoveredDeckUrls: interfaceDiscovery.getDiscoveredDeckUrls(),
interfaceBindingStores: config.interfaceBindingStores,

View file

@ -16,6 +16,7 @@ function buildConfig(argv) {
const args = parseArgs(argv);
const port = Number(args.port || 8797);
const host = args.host || "0.0.0.0";
const cultnetRudpBind = args["cultnet-rudp-bind"] ? String(args["cultnet-rudp-bind"]) : "";
const intervalMs = Number(args.intervalMs || 5000);
const stateDir = args.stateDir || path.join(repoRoot, "scratch", "odin");
const cachePath = args.cachePath || path.join(stateDir, "odin.ccmp");
@ -57,6 +58,7 @@ function buildConfig(argv) {
return {
args,
cachePath,
cultnetRudpBind,
gamecultTextDocumentStorePath,
host,
interfaceBindingStores,

229
src/odin/cultnet-rudp.cjs Normal file
View file

@ -0,0 +1,229 @@
"use strict";
const dgram = require("dgram");
const path = require("path");
const { createRequire } = require("module");
const requireCultNet = createRequire(path.resolve(
__dirname,
"..",
"..",
"..",
"CultLib",
"packages",
"cultnet-ts",
"package.json",
));
const {
CultNetDocumentRegistry,
CultNetRudpSession,
cultNetBuiltinSchemaRegistry,
decodeRudpPacket,
defineCultNetDocumentBinding,
encodeCultNetMessageForWire,
encodeRudpPacket,
parseCultNetMessage,
} = requireCultNet("./dist/index.js");
const { decode, encode } = requireCultNet("@msgpack/msgpack");
const ODIN_RUDP_CONNECTION_ID = 0x0d1d0002;
const DEFAULT_SESSION_TIMEOUT_MS = 30_000;
function createCultNetRudpSurfaceServer(options) {
if (!options?.bind) return null;
if (!options.documents?.surfaceDefinition) {
throw new Error("Odin CultNet/RUDP surface server requires a surface document definition.");
}
if (typeof options.getCache !== "function") {
throw new Error("Odin CultNet/RUDP surface server requires an async getCache function.");
}
const bind = parseEndpoint(options.bind);
const documentRegistry = new CultNetDocumentRegistry([
defineCultNetDocumentBinding({ definition: options.documents.surfaceDefinition }),
]);
const sessions = new Map();
const socket = dgram.createSocket(bind.host.includes(":") ? "udp6" : "udp4");
const resendPollMs = Math.max(10, Number(options.resendPollMs || 25));
const sessionTimeoutMs = Math.max(1_000, Number(options.sessionTimeoutMs || DEFAULT_SESSION_TIMEOUT_MS));
let resendTimer = null;
socket.on("message", (wire, remote) => {
try {
const packet = decodeRudpPacket(wire);
handlePacket(packet, remote).catch((error) => {
logError(options, `CultNet/RUDP packet handling failed for ${remote.address}:${remote.port}`, error);
});
} catch (error) {
logError(options, "CultNet/RUDP packet decode failed", error);
}
});
socket.on("error", (error) => {
logError(options, "CultNet/RUDP socket error", error);
});
async function start() {
await new Promise((resolve, reject) => {
socket.once("error", reject);
socket.bind(bind.port, bind.host, () => {
socket.off("error", reject);
resolve();
});
});
resendTimer = setInterval(() => {
try {
const nowMs = Date.now();
for (const [key, record] of sessions) {
if (record.session.checkTimeout(nowMs, sessionTimeoutMs)) {
sessions.delete(key);
continue;
}
for (const packet of record.session.dueResends(nowMs)) {
sendPacket(record.remote, packet);
}
}
} catch (error) {
logError(options, "CultNet/RUDP resend loop failed", error);
}
}, resendPollMs);
resendTimer.unref?.();
}
function close() {
if (resendTimer) {
clearInterval(resendTimer);
resendTimer = null;
}
sessions.clear();
socket.close();
}
async function handlePacket(packet, remote) {
const key = remoteKey(remote);
const nowMs = Date.now();
let record = sessions.get(key);
if (packet.packetType === "connect") {
record = {
remote: { address: remote.address, family: remote.family, port: remote.port },
session: new CultNetRudpSession({
connectionId: ODIN_RUDP_CONNECTION_ID,
initialSequence: 1,
resendDelayMs: 100,
}),
};
sessions.set(key, record);
sendPacket(record.remote, record.session.acceptConnect(packet, nowMs, new Uint8Array()));
return;
}
if (!record) {
return;
}
const result = record.session.receive(packet, nowMs);
if (result.reply) {
sendPacket(record.remote, result.reply);
}
for (const frame of result.delivered) {
if (frame.channelId !== "schema") {
continue;
}
await handleSchemaFrame(record, frame.payload);
}
if (result.disconnected) {
sessions.delete(key);
return;
}
if (packet.packetType === "accept" || result.delivered.length > 0) {
sendPacket(record.remote, record.session.createAck());
}
}
async function handleSchemaFrame(record, payload) {
const message = parseCultNetMessage(decode(payload), "cultnet.schema.v0");
switch (message.schemaVersion) {
case "cultnet.snapshot_request.v0": {
try {
const cache = await options.getCache();
const response = documentRegistry.createRawSnapshotResponse(cache, message.messageId, message);
sendSchemaMessage(record, response);
} catch (error) {
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: error.message,
});
}
return;
}
case "cultnet.schema_catalog_request.v0":
sendSchemaMessage(record, cultNetBuiltinSchemaRegistry.createCatalogResponse(message));
return;
default:
sendSchemaMessage(record, {
schemaVersion: "cultnet.error.v0",
error: `Unsupported Odin CultNet/RUDP request ${message.schemaVersion}.`,
});
}
}
function sendSchemaMessage(record, message) {
const payload = encode(encodeCultNetMessageForWire(message, "cultnet.schema.v0"));
for (const packet of record.session.sendMany("schema", payload, {
reliable: true,
ordered: true,
nowMs: Date.now(),
})) {
sendPacket(record.remote, packet);
}
}
function sendPacket(remote, packet) {
const wire = Buffer.from(encodeRudpPacket(packet));
socket.send(wire, remote.port, remote.address);
}
return {
bind,
start,
close,
};
}
function parseEndpoint(value) {
const text = String(value || "").trim();
const ipv6 = text.match(/^\[([^\]]+)\]:(\d+)$/);
if (ipv6) return { host: ipv6[1], port: parsePort(ipv6[2]) };
const index = text.lastIndexOf(":");
if (index <= 0) {
throw new Error(`CultNet/RUDP bind must be host:port, got "${value}".`);
}
return { host: text.slice(0, index), port: parsePort(text.slice(index + 1)) };
}
function parsePort(value) {
const port = Number(value);
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
throw new Error(`CultNet/RUDP port is invalid: ${value}`);
}
return port;
}
function remoteKey(remote) {
return `${remote.address}:${remote.port}`;
}
function logError(options, label, error) {
const message = error instanceof Error ? error.message : String(error);
if (typeof options?.logError === "function") {
options.logError(`${label}: ${message}`);
return;
}
console.error(`${label}: ${message}`);
}
module.exports = {
createCultNetRudpSurfaceServer,
};