diff --git a/src/Mimir.Runtime/Synchronization/MimirAsioStreamSource.cs b/src/Mimir.Runtime/Synchronization/MimirAsioStreamSource.cs index c3a9b73..308ca04 100644 --- a/src/Mimir.Runtime/Synchronization/MimirAsioStreamSource.cs +++ b/src/Mimir.Runtime/Synchronization/MimirAsioStreamSource.cs @@ -108,8 +108,6 @@ public sealed class MimirAsioStreamSource : IMimirMultiplexedStreamSource started.Set(); var sampleBuffer = new float[Math.Max(1, maxFrames)]; - long firstDeviceTimestampNs = 0; - long firstCanonicalEndNs = 0; while (!cancellation.IsCancellationRequested) { if (!Native.Read( @@ -134,16 +132,7 @@ public sealed class MimirAsioStreamSource : IMimirMultiplexedStreamSource var bytes = new byte[frameCount * sizeof(float)]; var arrivalNs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; var durationNs = checked((long)Math.Ceiling(frameCount * 1_000_000_000.0 / Math.Max(1, sampleRate))); - if (timestampNs > 0 && firstDeviceTimestampNs == 0) - { - firstDeviceTimestampNs = timestampNs; - firstCanonicalEndNs = arrivalNs; - } - - var endNs = timestampNs > 0 && firstDeviceTimestampNs > 0 - ? checked(firstCanonicalEndNs + (timestampNs - firstDeviceTimestampNs)) - : arrivalNs; - var startNs = Math.Max(0, endNs - durationNs); + var startNs = Math.Max(0, arrivalNs - durationNs); MemoryMarshal.AsBytes(sampleBuffer.AsSpan(0, frameCount)).CopyTo(bytes); samples.Enqueue(new MimirStreamSample( sourceId, diff --git a/src/Mimir.Runtime/Synchronization/MimirFfmpegPcmAudioStreamSource.cs b/src/Mimir.Runtime/Synchronization/MimirFfmpegPcmAudioStreamSource.cs index 62c0484..b3959b2 100644 --- a/src/Mimir.Runtime/Synchronization/MimirFfmpegPcmAudioStreamSource.cs +++ b/src/Mimir.Runtime/Synchronization/MimirFfmpegPcmAudioStreamSource.cs @@ -96,7 +96,7 @@ public sealed class MimirFfmpegPcmAudioStreamSource : IMimirStreamSource offset += read; } - var timestampNs = StopwatchTicksToNs(Stopwatch.GetTimestamp()); + var timestampNs = UtcNowNs(); samples.Enqueue(new MimirStreamSample( Descriptor.SourceId, Descriptor.Kind, @@ -137,10 +137,7 @@ public sealed class MimirFfmpegPcmAudioStreamSource : IMimirStreamSource _ => 0, }; - private static long StopwatchTicksToNs(long ticks) - { - return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency))); - } + private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; public void Dispose() { diff --git a/src/Mimir.Runtime/Synchronization/MimirFfmpegRawVideoStreamSource.cs b/src/Mimir.Runtime/Synchronization/MimirFfmpegRawVideoStreamSource.cs index 31c6052..4c08519 100644 --- a/src/Mimir.Runtime/Synchronization/MimirFfmpegRawVideoStreamSource.cs +++ b/src/Mimir.Runtime/Synchronization/MimirFfmpegRawVideoStreamSource.cs @@ -98,7 +98,7 @@ public sealed class MimirFfmpegRawVideoStreamSource : IMimirStreamSource offset += read; } - var timestampNs = StopwatchTicksToNs(Stopwatch.GetTimestamp()); + var timestampNs = UtcNowNs(); var videoFrame = new MimirVideoFrameDescriptor( Options.Width, Options.Height, @@ -152,10 +152,7 @@ public sealed class MimirFfmpegRawVideoStreamSource : IMimirStreamSource }; } - private static long StopwatchTicksToNs(long ticks) - { - return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency))); - } + private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; public void Dispose() { diff --git a/src/Mimir.Runtime/Synchronization/MimirFrameEventProcessStreamSource.cs b/src/Mimir.Runtime/Synchronization/MimirFrameEventProcessStreamSource.cs index 134203d..3556cff 100644 --- a/src/Mimir.Runtime/Synchronization/MimirFrameEventProcessStreamSource.cs +++ b/src/Mimir.Runtime/Synchronization/MimirFrameEventProcessStreamSource.cs @@ -118,8 +118,8 @@ public sealed class MimirFrameEventProcessStreamSource : IMimirStreamSource var timestampNs = frameEvent.TimestampNs > 0 ? frameEvent.TimestampNs - : StopwatchTicksToNs(Stopwatch.GetTimestamp()); - var arrivalNs = StopwatchTicksToNs(Stopwatch.GetTimestamp()); + : UtcNowNs(); + var arrivalNs = UtcNowNs(); var byteLength = Math.Max(0, frameEvent.ByteLength); var payloadHandle = frameEvent.NativeHandle; var sampleKind = frameEvent.IsAudioBlock ? MimirStreamKind.Audio : Descriptor.Kind; @@ -271,10 +271,7 @@ public sealed class MimirFrameEventProcessStreamSource : IMimirStreamSource }; } - private static long StopwatchTicksToNs(long ticks) - { - return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency))); - } + private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; public void Dispose() { diff --git a/src/Mimir.Runtime/Synchronization/MimirProcessStreamSource.cs b/src/Mimir.Runtime/Synchronization/MimirProcessStreamSource.cs index fbe8c52..79a2d69 100644 --- a/src/Mimir.Runtime/Synchronization/MimirProcessStreamSource.cs +++ b/src/Mimir.Runtime/Synchronization/MimirProcessStreamSource.cs @@ -74,8 +74,7 @@ public sealed class MimirProcessStreamSource : IMimirStreamSource Array.Resize(ref buffer, read); } - var now = Stopwatch.GetTimestamp(); - var timestampNs = StopwatchTicksToNs(now); + var timestampNs = UtcNowNs(); samples.Enqueue(new MimirStreamSample( Descriptor.SourceId, Descriptor.Kind, @@ -102,10 +101,7 @@ public sealed class MimirProcessStreamSource : IMimirStreamSource } } - private static long StopwatchTicksToNs(long ticks) - { - return checked((long)(ticks * (1_000_000_000.0 / Stopwatch.Frequency))); - } + private static long UtcNowNs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000L; public void Dispose() { diff --git a/src/Mimir.Well/Program.cs b/src/Mimir.Well/Program.cs index 365bc75..63319f9 100644 --- a/src/Mimir.Well/Program.cs +++ b/src/Mimir.Well/Program.cs @@ -1558,11 +1558,18 @@ internal static class MimirWellSnapshot TryReadAnonymousString(error, "SourceId", "sourceId") is { } id && string.Equals(id, descriptor.SourceId, StringComparison.Ordinal)); var elapsedMs = Math.Max(0.0, (DateTimeOffset.UtcNow - startedAt).TotalMilliseconds); + var latestAgeMs = latest.HasValue + ? Math.Round(Math.Max(0.0, (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000.0 - latest.Value.TimestampNs) / 1_000_000.0), 1) + : Math.Round(elapsedMs, 1); var status = errored ? "create-error" - : samples > 0 - ? "live" - : "created-no-samples"; + : samples > 0 && latestAgeMs > 2_000.0 + ? "stalled" + : samples > 0 + ? "live" + : descriptor.Origin == MimirStreamOrigin.Network + ? "waiting-network" + : "waiting-device-samples"; return new { @@ -1574,15 +1581,15 @@ internal static class MimirWellSnapshot reason = status switch { "create-error" => "source creation failed; see sourceErrors", - "created-no-samples" => "source was created but has not emitted a sample", + "stalled" => "source emitted samples earlier but latest sample is stale", + "waiting-network" => "network source listener is configured but no producer samples have arrived", + "waiting-device-samples" => "local device source is configured but has not emitted a sample", _ => "samples arriving", }, buffers = matchingBuffers.Length, samples, latestTimestampNs = latest?.TimestampNs ?? 0L, - latestAgeMs = latest.HasValue - ? Math.Round(Math.Max(0.0, (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() * 1_000_000.0 - latest.Value.TimestampNs) / 1_000_000.0), 1) - : Math.Round(elapsedMs, 1), + latestAgeMs, latestByteLength = latest?.ByteLength ?? 0, }; } diff --git a/state/evidence.jsonl b/state/evidence.jsonl index 7c5feee..43139a1 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -367,3 +367,4 @@ {"ts": "2026-06-02T19:17:54+01:00", "type": "decision", "status": "ok", "note": "Online Verse shakedown: canonical timestamps are owned by MimirCanonicalClockMapper, which now rebases streams whose projected source clock drifts more than 10s from arrival after ASIO/device resets; MimirSynchronizationHub no longer reapplies raw clock-domain corrections to already-canonical buffers. Fensalir Eve surface now mirrors sibling daemon PID liveness so Odin can lower relay, recorder, Well, Move sync, and Fensalir statuses without taking control authority."} {"ts": "2026-06-02T23:49:45+01:00", "type": "decision", "status": "ok", "note": "Mimir Well/Fensalir live repair: rolling-buffer authority now uses canonical arrival UTC timestamps for ASIO/native camera samples while preserving device timestamps as calibration evidence; Fensalir CultCache persistence is best-effort so a Windows File.Replace sharing failure cannot kill the live Eve daemon."} {"ts": "2026-06-03T04:13:02+01:00", "type": "implementation", "status": "ok", "note": "2026-06-03: ASIO readiness repair: Mimir now derives ASIO block times from native device timestamps, preserves local-epoch timestamps in the canonical mapper, sorts planner samples chronologically, keeps frames visible when overlap pinches, and lets the latency controller chase FutureSampleOnly slices downward. Live Well reached repeated 6/6 Ready frames at roughly 75-80 ms presentation delay with ASIO distances reported around 32-36 ms."} +{"ts": "2026-06-03T04:40:48+01:00", "type": "implementation", "status": "ok", "note": "2026-06-03: Source health and process-clock repair: FFmpeg/process/frame-event sources now use UTC arrival timestamps instead of Stopwatch-domain timestamps; Well sourceHealth distinguishes live, stalled, waiting-network, and waiting-device-samples. Live verification after restart reached repeated 6/6 Ready frames, while streamPressure exposed the next bottleneck: inline WebSocket body lane published ~66 MB across 53 Well snapshots at ~82 ms average publish cost."}