One table, 15 records: each encodes to its pinned positional MessagePack
array (dropped, swapped, retyped keys change the bytes), and every record
with a defaulted tail decodes from a payload that ends before each
defaulted key, one truncation per key, reading the rest as zero values.
Replaces the two hand-written hid and hue tests and the single hue
truncation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The wire-layout goldens set every field, so dropping `default` from a
record's tail survived them. Adds a test that decodes an eight-element
hue program payload and expects the defaulted tail.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New library crate crates/muninn-contracts holds the records Muninn owns
(telemetry surface, capture stream and command, OBS catalog, Move marker
candidate, controller state, identity, light command, hue program,
tracker health and evidence transport health, HID controller state,
Quest access, command boundary, transport profile), their schema
constants, and the MuninnRecords document set. The definitions are the
odin-core ones moved verbatim; the wire is unchanged.
muninn and sleipnir import the records from here, so both link one
definition. muninn registers MuninnRecords after OdinDocuments, which
stays because it still carries Odin's own records and stale copies of
the same schema ids. Muninn's odin-core imports are now
EveProviderAdvertisementRecord, EveSurfaceStateRecord and OdinDocuments.
Tests pin every type name and schema id, the document-set bindings, and
the positional wire layout of two records.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit left health_check computing an observed_at and a
state/detail pair whose only consumer was the deleted publisher; the
timestamp helper itself was deleted, so the workspace did not build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The flag family (--idunn-rudp-health, --idunn-daemon,
--idunn-health-contract), IdunnRudpHealthOptions, the health worker
thread, idunn_health_payload, the RUDP health connection and the
idunn.daemon_health document had no consumer since the previous Idunn
generation. The raven-muninn recipe never passed them; liveness is the
runtime-presence publisher. Removes the IdunnDaemonHealthRecord odin-core
symbol (three remain).
The daemon id is now derived from the host alone. The transport profile
and command-boundary records no longer carry health_publication, and
health_contract is null. The starfire, nightwing and raven health and
restart scripts stop passing and checking the flags; muninn.md follows.
Tests: the retired flags are unknown arguments; the boundary record
carries the host-derived daemon id and no health_publication.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Audio priority per payload, deadlines per payload, every sender-side drop
consumes a frame_id, the latency budget clamped to 2000 ms, a departed
receiver drops out without stopping the others. Soul pass 3: merge with
follow-ups (mid-group leaver order unpinned; per-receiver I/O errors still
stop the loop; raw budget persists in the command record).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A request naming no budget stores 0 and means the 250 ms default; clamping at
the copy turned 0 into 1 ms. The command accessor already owns 0-means-default,
so it owns the clamp too: compare, spawn arguments and logs read through it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A receiver that timed out or disconnected between payloads left a stale
session in the in-flight group snapshot and in queued repairs; the next
hub.send returned "Unknown RUDP server peer" and the mux restarted for every
receiver. try_send now says whether the session still exists (address and
generation) and reports ReceiverGone; the group drops that receiver, the repair
queue drops that receiver's repairs, and every other hub error still
propagates.
Tests that fail under their mutation: the three departure shapes, audio
expiry, a repair kept on a full socket, resends polled on a full socket, the
loss-to-repair-budget wiring, and a leftover spanning two pushes.
The latency budget is clamped by one function where a request or a command line
is read, so the advertisement, telemetry and stream-equivalence check all see
the clamped value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The extra room cap on queued repairs had no test and no owner beyond the
budget and the frame deadline, which already bound the queue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The send loop now moves one payload per step and chooses it afresh: audio,
then repairs, then video. A video group in progress or a full socket no
longer holds audio back for more than the payload in progress; a full
socket hands control back instead of spinning to the group deadline.
Repairs are queued by feedback intake and sent by the same step, each
checked against its own frame's deadline at send time and only to the
receiver that asked. The group deadline is checked before every payload; a
group that outlives it mid-send is abandoned and counted groups_cut_short.
A request's latency budget is clamped to 1..=2000 ms.
Tests pin the cap values, the clamp, per-payload audio priority (mid-frame,
full socket, between repairs), the send-time deadline for groups and
repairs, repairs going only to the asker, and the frame-id gap left by a
queue-cap, expiry or overflow drop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An access unit dropped for outgrowing max_pending_bytes consumed no frame
id, so a receiver could not see the gap, and the slices that followed it
were emitted as a complete access unit of their own. The drop now spends
the frame id (and its pts step) and the state discards the leftover slices
up to the next access-unit boundary, waiting for the boundary when the
leftover is not yet complete. A stream that resumes on a boundary loses
nothing more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul: merge with follow-ups. The CultLib c2a9a6e pin stops acking
unreliable frames, so Sleipnir's 1 s subscribe alone refreshes Muninn's 2 s
HID timeout; H2/H4 replace that path. odin-core 3e96c6c lives only under an
attic tag; repin once the bump reaches Odin main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Odin main's Sleipnir was written against CultLib a8aedda, and its two
CultNetRudpSocketTransportOptions initialisers do not compile at
Muninn's c2a9a6e pin. media_delivery: None is the field's default, so
behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sleipnir joins the workspace at Muninn's pins (CultLib c2a9a6e, odin-core
Odin 3e96c6c) instead of Odin main's a8aedda and a path dependency.
Muninn and Sleipnir now move together by construction. Manifests and
lock only; no source change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges crates/sleipnir-daemon/{Cargo.toml,src/main.rs} filtered from Odin
main 379b826 (git filter-repo --path, unrelated history). Source is
unchanged; the manifest still carries Odin's pins and is repointed in the
next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The input firewall rule is what any producing Muninn needs, not an input
exception or an accepted cost of Q10.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The streaming doc still said a request naming no latency budget got 2,000 ms
and did not mention that the sender queue is bounded, per-group, and owns the
deadline that repairs inherit. Re-anchor the two main.rs references it moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sender's queues were unbounded (mpsc::channel plus two VecDeques), expired
payloads one at a time so half a frame could leave after the other half died,
gave repairs a fresh deadline from the clock, spun on a 1 ms sleep, and could
not be driven without PowerShell, ffmpeg and WASAPI. The latency budget also had
two owners: a 2 s producer default that the receiver's 250 ms age never matched.
MediaSendCore (new media_send.rs) owns the queues, hub, feedback intake, repair
cache and budget, and progress counters. run_rudp_mux_once now spawns encoders,
feeds readers into the core and publishes; it decides no deadline.
- The unit is a group: one access unit with its parity, or one audio packet,
stamped produced_at + the request's latency budget when it leaves the
packetizer. Expiry is per group; a group is sent whole or not at all.
- Queues are capped per kind (audio 256, video 512 groups), drop the oldest
group and count it (groups_dropped_audio, groups_dropped_video). The reader
channel is a bounded sync_channel.
- The Annex B packetizer returns one group per access unit and no longer errors
on pending overflow: an unfinished access unit larger than the ceiling is
dropped and counted.
- Repairs inherit the deadline of their frame and are not sent after it. A
blocked send polls resends and stops at the group deadline; the sleep is gone.
- MUNINN_RUDP_MEDIA_RECEIVER_ASSEMBLY_DEADLINE_MS is deleted. The default budget
is 250 ms (Q7), used only when a request names none and offered as the
advertisement's default_latency_budget_ms. The profile loses its three copies
of the budget; the telemetry JSON carries one latency_budget_ms.
- A test harness drives the real readers and core over loopback through a
seeded drop relay to a CultNet RUDP client, from a committed H.264 clip
(testdata/clip.h264, ffmpeg command recorded beside it) and a PCM ramp.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sleipnir moves into the Muninn workspace with its history. Input mirrors
the media request path: Muninn advertises an input stream on CultMesh, the
consumer requests it and dials. Opens Q11-Q13.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Soul: merge after doc fixes (done in b607592). -565 lines from the producer;
no consumer of the deleted symbols in Muninn, Ratatoskr, Odin, Mimir or
StreamPixels; the bluetoothctl-dependent test now runs through a seam.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
video_wire_records_with_parity keeps only its single-frame caller. The stale
move-identity prune is split out of reconcile_move_identity_records so its
test no longer shells out to the host Bluetooth binary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cut 6: the donor encoder behind a C ABI in an off-default package, typed
encoder-command records on stdin, built and accepted on Raven by Idunn. HID:
muninn-contracts crate, edges carried on the latest channel with the state, no
Odin main change. Q9 (Sleipnir's home) and Q10 (who dials) open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delete AudioPacketBuffer (receiver jitter buffer), the whole-stream Annex B
helpers (packetize_video_annex_b_stream, its options structs and wire
wrappers) and encode/decode_record_payload; only tests called them. Re-home
the packetizer-behaviour tests onto VideoAnnexBStreamSendState. Drop the five
audio_packet_buffer tests and the legacy-feedback decode test (CultLib pins
key 10 default in receiver_feedback_decodes_records_written_before_its_last_field).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
teardown-map.md: mark the extraction-era claims as history in place (pin,
symbol counts, diamond, live-exercise note) and correct the false claim that
the sender turns keyframe requests into an IDR. muninn-media-streaming.md:
preset p5, 848-byte chunks, 30 ms resend inside the latency-budget expiry.
Docs only.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operator made the Odin muninn-bounded-realtime-send donor a campaign. FEC moves
to a CultLib cultnet-rs media_fec codec (reed-solomon-erasure) used by both
Muninn and Ratatoskr; bounded whole-frame sender; one latency owner. Q1-Q8 open.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Idunn no longer infers how a target proves readiness, and a recipe with
neither a route nor an Odin dependency is refused at admission. raven-muninn
has no route and publishes to Odin over CultMesh RUDP, so its recipe declares
the odin.verse-rendezvous dependency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
restart-muninn.ps1, its cmd alias, deploy-raven-muninn-binary.ps1 and
repair-raven-muninn-task-actions.ps1 started and replaced serve on Raven by
hand through a scheduled task and a VBS launcher. Idunn's host actuator is
the only thing that starts serve there now, so they leave rather than stay
as a second launch path. Docs say what replaced them and what remains.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Odin admits a signer's presence only above the sequence it last stored,
across the signer's restarts. The sequence is now the observation clock,
which a fresh process cannot restart below.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A switched-off gamepad made --health fail and so kept the Idunn presence
at warming forever; the daemon was serving the whole time. Presence now
reads the telemetry surface the tick rewrites; source freshness stays in
--health where an operator asked for it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The committed workspace patch pointed CultLib at ../CultLib, which only a
workstation with that checkout beside it could satisfy; on Raven the
build died looking for it. odin-core now comes from Odin 3e96c6c, whose
CultLib pin is c2a9a6e like ours, so the diamond closes without a patch.
A developer who wants a local CultLib puts the patch in .cargo/config.toml.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When Idunn's host actuator starts serve, it hands over the runtime bundle,
a one-shot activation credential and the host's provider identity. From
those, serve publishes a dual-signed gamecult.runtime_presence_health.v2
record to Odin every two seconds: warming until its own health check
passes, active after. Odin's correlation of that record is what admits
the incarnation and keeps it alive. An operator-started serve has no
bundle and publishes nothing.
deployment/idunn/raven-muninn.toml is the target declaration: one cargo
build step on the host, the daemon and the loopback script as artifacts,
and the launch contract that today's restart script assembles by hand,
with Raven's paths as argument bindings. Cargo.lock is committed so the
build is --locked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Windows hands a priority class down to a child only when it is below
normal, so the mux child's above-normal never reached ffmpeg; on a live
stream with the host at 99.8% CPU the encoder ran at normal, 790 video
chunks per 10 s went missing and the mux loop sat a second behind. Raising
the processes in place took that to 116 lost and a 60-chunk backlog with
zero late frames, and the viewer reported every frame arriving. The mux
child and video encoder now run at high, the audio pipeline at
above-normal, from spawn.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
serve is the body that must outlive a racing store write or an absent
Odin. It exited on the first "failed to atomically replace" and left its
activation child streaming with nobody answering requests; OBS's own
request sat at pending while it watched the orphan. The tick body now
reports and the loop continues on the next interval. CultLib pin c2a9a6e
carries the store-side retry for the same race.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The "kept existing capture stream" branch wrote the running command back
with the current time on every tick, which made a running command the
newest forever: no request sent tonight could outrank the July start until
this was cut. The spawn transition keeps the command's stamp too. The
teardown map records the audio result: 860 frames and 2,756 audio packets
in 29 s, nothing expired, on a producer host at 93% CPU.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The command engine orders commands by updated_at, and both terminal
writers, superseding and reaping, re-stamped the dead record with the
current time. A superseded command therefore outranked the command that
had superseded it on the very next tick and respawned; tonight a July
start did that to the stop that ended it. The stamp is the command's
ordinal now: superseding and reaping change state and detail only, with
the exit time carried in the detail. A newer command that was already
handled still shadows an older pending one, which the earlier cut had
broken and the existing test caught.
CultLib pin 47b9fc4: readers no longer stop at acknowledgement-only
packets, which was what starved the video leg once reliable audio shared
the session.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5 ms resend was a July LAN value for the old dialled client. A receiver
that polls every 2 ms answers in 3-6 ms, so nearly every audio fragment
was resent before its acknowledgement arrived and the producer's send
loop drowned in retransmits: 7 video frames in 9 s with audio on, 266
without. Audio expires at the assembly deadline regardless.
The activation child now raises itself to above-normal priority, which
its encoder and capture children inherit. Measured on Raven at 93% total
CPU with a game running: at normal priority the mux loop starved and
video backed up 1.5 s before expiring wholesale.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>