From 27ec51ec9a126761320860f18839d55c8a8a295c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 4 Jun 2026 18:32:26 +0100 Subject: [PATCH] Add Idunn keepalive organ --- README.md | 16 +++++++ docs/architecture.md | 12 +++++ docs/idunn.md | 102 +++++++++++++++++++++++++++++++++++++++++ package.json | 2 + src/Idunn/Idunn.csproj | 12 +++++ src/Idunn/Program.cs | 102 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 246 insertions(+) create mode 100644 docs/idunn.md create mode 100644 src/Idunn/Idunn.csproj create mode 100644 src/Idunn/Program.cs diff --git a/README.md b/README.md index c2f38a7..fec42e8 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,22 @@ Local package surfaces: - Avatar asset: `assets/personas/gjallar-avatar.png` - Pixel avatar: `assets/personas/gjallar-avatar-pixel-256.png` +## Idunn + +Idunn is Odin's keepalive organ: the daemon package that should keep the known +swarm alive after Odin has accepted where each daemon lives and what authority +path may touch it. Individual daemons publish health and command boundaries; +Idunn brings them up after reboots or crashes, watches health, and escalates +operator-needed failures through VoidBot's CultMesh owner-DM command. Odin sees +the daemons; Idunn keeps the apples from rotting. + +Local package surfaces: + +- Organ contract: `docs/idunn.md` +- C# CultMesh organ: `src/Idunn/Idunn.csproj` +- Runtime store: `scratch/idunn/idunn.keepalive.cc` +- Operator escalation: `voidbot.operator-dm` command `owner.dm.send` + ## Authority Map - Owner: Odin owns the network-wide Verse registry, schema catalog index, translation map, and the accepted `odin.allseer` Eve surface. diff --git a/docs/architecture.md b/docs/architecture.md index fa7a5dc..0ad163c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -78,6 +78,18 @@ Odin's coordinator. Gjallar may read Odin-owned state and emit affordance packets; it must not own the underlying registry, probe, provider, layout, or translation decisions. +Idunn is the named keepalive organ for daemon continuity. Its current package +lives in `docs/idunn.md` and `src/Idunn/Idunn.csproj`. Idunn may read +Odin-owned service records and provider advertisements, then bring daemons up +after reboots or crashes, watch health, emit keepalive observations, restart +requests, denied-action records, and operator alarms. When human action is +needed, Idunn should use CultMesh to request VoidBot's provider-owned +`voidbot.operator-dm` command `owner.dm.send` with payload schema +`gamecult.operator_dm_request.v1`. It must not own Verse discovery, schema +truth, provider dashboards, identity grants, Discord delivery, or renderer +layout. Keepalive loops should move into Idunn instead of accumulating inside +Odin's coordinator or individual daemons. + ## Target Mechanism ```text diff --git a/docs/idunn.md b/docs/idunn.md new file mode 100644 index 0000000..5887f95 --- /dev/null +++ b/docs/idunn.md @@ -0,0 +1,102 @@ +# Idunn + +Idunn is Odin's keepalive organ. + +Odin is the all-seer: it accepts Verse discovery, schema catalogs, translation +routes, provider surfaces, and observation projections. Idunn keeps the daemon +swarm alive from that accepted map. It is not a second Odin, not a dashboard, +and not a heroic supervisor with secret service truth in its pockets. + +Idunn keeps the apples: desired daemon presence, boot recovery, crash recovery, +health freshness, restart intent, operator escalation, and continuity witness +state. + +## Authority Map + +- Owner: Idunn owns daemon lifecycle management after Odin has accepted where a + daemon lives: bring-up after host reboot, crash recovery, health watching, + restart policy, and operator escalation. +- Inputs: Odin's accepted service records, provider advertisements, `.cc` + witnesses, advertised command boundaries, freshness windows, operator policy, + local service manager state, and direct local service probes only when no + provider advertisement exists. +- Outputs: typed keepalive observations, restart requests, denied-action + records, operator alarms, owner-DM escalation requests through VoidBot, and an + Eve/CultUI keepalive surface. +- Derived state: dashboard cells, Discord speech, agent summaries, and Odin + service projections are notification-only views of Idunn-owned keepalive + records. +- Forbidden writers: Idunn does not decide which Verses exist, invent provider + schemas, mutate provider dashboards, own identity/session grants, or hide + restarts behind Odin refresh logic. Individual daemons should not carry + independent crash-recovery loops once Idunn owns their lifecycle path; they + publish health, surfaces, state witnesses, and command boundaries instead. +- Shared paths: manual operator restart, scheduled restart, degraded-health + repair, boot rehydration, and future remote worker recovery must pass through + the same keepalive command primitive. +- Deletion line: any keepalive loop inside Odin, Gjallar, Eve lowerers, or + renderer code should be cut or demoted to a probe that names Idunn as the + restart owner. + +## Body + +The initial local package is deliberately small: + +- `src/Idunn/Idunn.csproj` is the first C# CultMesh organ. +- `src/Idunn/Program.cs` opens or creates Idunn's local CultCache/CultMesh + store at `scratch/idunn/idunn.keepalive.cc`. +- `npm run idunn:build` builds the organ beside `gjallar:build`. +- `npm run idunn:start -- --serve` starts the resident keepalive process. + +This is the organ contract and runtime foothold. It does not yet restart +anything. The first real keepalive cut should add typed records before adding +actuators: + +```text +idunn.desired_daemon.v1 +idunn.daemon_health.v1 +idunn.keepalive_decision.v1 +idunn.restart_request.v1 +idunn.restart_result.v1 +idunn.operator_alarm.v1 +idunn.owner_dm_escalation.v1 +``` + +## Invariants + +- Odin remains the accepted owner of Verse and service discovery. +- Idunn owns continuity decisions after a daemon is known. +- Individual daemons own their work and their health publication, not their + surrounding lifecycle. They should be simple to kill and simple for Idunn to + bring back. +- Providers own their own command boundaries. Idunn requests restart through + advertised authority or a named local service manager adapter. +- Host reboot recovery, crash recovery, stale-health recovery, and manual + operator restart must share the same Idunn command primitive. +- A repair loop is not an owner. If a daemon becomes healthy only after a later + Odin refresh or manual click, Idunn's ownership path is still incomplete. +- Restart attempts must be witnessed: requested by whom, against which service, + through which command boundary, with what result and timestamp. +- Idunn should fail closed when authority is unclear. Unknown ownership, + repeated restart failure, missing command authority, or degraded health that + needs a human becomes an operator alarm. +- Operator alarms use CultMesh to request VoidBot owner notification through + the provider-owned `voidbot.operator-dm` command `owner.dm.send` with payload + schema `gamecult.operator_dm_request.v1`. Idunn must not learn Discord token + handling or DM delivery internals. + +## First Runtime Direction + +Idunn's C# runtime should grow in this order: + +1. Read Odin-owned service and provider-advertisement records through CultMesh. +2. Normalize them into `idunn.desired_daemon.v1` records. +3. Observe freshness and process state through narrow probe ports. +4. Emit `idunn.keepalive_decision.v1` records without acting. +5. Add restart adapters only for named authority boundaries: local systemd, + Windows service control, Docker, or provider-advertised commands. +6. Add `voidbot.operator-dm` escalation for cases that need operator action. +7. Publish an Eve/CultUI keepalive surface from Idunn-owned records. + +No ad hoc JSON manifest should become the live state owner. Debug projections +are fine when they name the `.cc` record or CultMesh document behind them. diff --git a/package.json b/package.json index c5680b2..28f5d55 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "gjallar:persona": "node scripts/write-gjallar-persona-cc.mjs inspect", "gjallar:persona:write": "node scripts/write-gjallar-persona-cc.mjs write", "gjallar:build": "dotnet build src/Gjallar/Gjallar.csproj", + "idunn:build": "dotnet build src/Idunn/Idunn.csproj", + "idunn:start": "dotnet run --project src/Idunn/Idunn.csproj --", "rust:test": "cargo test --workspace", "check": "node scripts/check-syntax.cjs", "start": "node src/odin-coordinator.cjs" diff --git a/src/Idunn/Idunn.csproj b/src/Idunn/Idunn.csproj new file mode 100644 index 0000000..0ad5a93 --- /dev/null +++ b/src/Idunn/Idunn.csproj @@ -0,0 +1,12 @@ + + + Exe + net10.0 + enable + enable + + + + + + diff --git a/src/Idunn/Program.cs b/src/Idunn/Program.cs new file mode 100644 index 0000000..6973255 --- /dev/null +++ b/src/Idunn/Program.cs @@ -0,0 +1,102 @@ +using GameCult.Caching.MessagePack; +using GameCult.Mesh; + +var options = IdunnOptions.Parse(args); +var cachePath = Path.GetFullPath(options.CachePath); +var cacheDirectory = Path.GetDirectoryName(cachePath); + +if (!string.IsNullOrWhiteSpace(cacheDirectory)) +{ + Directory.CreateDirectory(cacheDirectory); +} + +using var node = await CultMesh.CreateNodeAsync( + cachePath, + new CultMeshNodeOptions + { + StartServer = options.Serve, + CacheOptions = new CultCacheOpenOptions + { + PullOnOpen = false + } + }).ConfigureAwait(false); + +Console.WriteLine("Idunn CultMesh organ initialized."); +Console.WriteLine($"Keepalive store: {cachePath}"); +Console.WriteLine("Provider id: idunn.keepalive"); +Console.WriteLine("Authority: daemon lifecycle, health watching, restart intent, and operator escalation for Odin-known daemons"); +Console.WriteLine("Owner escalation: CultMesh command voidbot.operator-dm owner.dm.send"); +Console.WriteLine($"CultMesh server: {(options.Serve ? "started" : "disabled")}"); +Console.WriteLine("Typed keepalive records and restart adapters are the next cut."); + +if (!options.Serve) +{ + return 0; +} + +var stopping = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +Console.CancelKeyPress += (_, eventArgs) => +{ + eventArgs.Cancel = true; + stopping.TrySetResult(); +}; + +Console.WriteLine("Press Ctrl+C to stop Idunn."); +await stopping.Task.ConfigureAwait(false); +return 0; + +internal sealed record IdunnOptions(string CachePath, bool Serve) +{ + public static IdunnOptions Parse(string[] args) + { + var repoRoot = FindRepoRoot(Directory.GetCurrentDirectory()); + var cachePath = Path.Combine(repoRoot, "scratch", "idunn", "idunn.keepalive.cc"); + var serve = false; + + for (var index = 0; index < args.Length; index++) + { + switch (args[index]) + { + case "--serve": + serve = true; + break; + case "--cache" when index + 1 < args.Length: + cachePath = args[++index]; + break; + case "--help": + case "-h": + PrintUsage(); + Environment.Exit(0); + break; + default: + throw new ArgumentException($"Unknown Idunn argument: {args[index]}"); + } + } + + return new IdunnOptions(cachePath, serve); + } + + private static string FindRepoRoot(string start) + { + var current = new DirectoryInfo(start); + while (current != null) + { + if (File.Exists(Path.Combine(current.FullName, "package.json")) && + Directory.Exists(Path.Combine(current.FullName, "src", "Gjallar"))) + { + return current.FullName; + } + + current = current.Parent; + } + + return start; + } + + private static void PrintUsage() + { + Console.WriteLine("Idunn"); + Console.WriteLine(" --cache Keepalive CultCache store path."); + Console.WriteLine(" --serve Start the CultMesh server instead of only opening the node."); + } +}