Add Idunn keepalive organ

This commit is contained in:
Meta Vens 2026-06-04 18:32:26 +01:00
commit 27ec51ec9a
6 changed files with 246 additions and 0 deletions

View file

@ -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.

View file

@ -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

102
docs/idunn.md Normal file
View file

@ -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.

View file

@ -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"

12
src/Idunn/Idunn.csproj Normal file
View file

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Mesh\GameCult.Mesh.csproj" />
</ItemGroup>
</Project>

102
src/Idunn/Program.cs Normal file
View file

@ -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 <path> Keepalive CultCache store path.");
Console.WriteLine(" --serve Start the CultMesh server instead of only opening the node.");
}
}