diff --git a/backend.tests/DeviceHubWakeFlowTests.cs b/backend.tests/DeviceHubWakeFlowTests.cs index 8d013fc..497fe88 100644 --- a/backend.tests/DeviceHubWakeFlowTests.cs +++ b/backend.tests/DeviceHubWakeFlowTests.cs @@ -52,11 +52,11 @@ public class DeviceHubWakeFlowTests(ApiFactory factory) : IClassFixture ReceiveUntil(WebSocket ws, string type) + { + for (int i = 0; i < 50; i++) + { + var env = await ReceiveJson(ws); + if (env.GetProperty("type").GetString() == type) return env; + } + throw new TimeoutException($"never saw envelope type={type}"); + } } diff --git a/backend/Realtime/RealtimeSession.cs b/backend/Realtime/RealtimeSession.cs index 8ea67a7..61ee09e 100644 --- a/backend/Realtime/RealtimeSession.cs +++ b/backend/Realtime/RealtimeSession.cs @@ -120,15 +120,47 @@ public class RealtimeSession private async Task PumpAsync(CancellationToken ct) { + int nullPolls = 0; while (!ct.IsCancellationRequested && !_endRequested) { var evt = await ReceiveOrIdleAsync(ct); if (evt is null && _endRequested) return; if (evt is null && ct.IsCancellationRequested) return; - if (evt is null) continue; + if (evt is null) + { + // Upstream has gone silent. If null persists for >2s, log so + // we can spot a dead upstream that's not surfacing as a close. + if (++nullPolls % 8 == 0) + _logger.LogDebug("upstream silent ({Polls} x 250ms polls)", nullPolls); + continue; + } + nullPolls = 0; var type = (string?)evt["type"]; + _logger.LogInformation("relay evt={EvtType}", type); + // Forward the event type to the device so it appears in the Pi + // journal — Coolify logs aren't accessible from the dev sandbox. + await _output.WriteEnvelopeAsync(new JsonObject + { + ["type"] = "debug", + ["message"] = "relay evt=" + (type ?? ""), + }, ct); switch (type) { + case "error": + { + var err = evt["error"] as JsonObject; + var code = (string?)err?["code"] ?? "unknown"; + var msg = (string?)err?["message"] ?? evt.ToJsonString(); + _logger.LogWarning("upstream mid-session error code={Code} message={Message}", code, msg); + await _output.WriteEnvelopeAsync(new JsonObject + { + ["type"] = "error", + ["code"] = "upstream_" + code, + ["message"] = msg, + ["fatal"] = false, + }, ct); + break; + } case "input_audio_buffer.speech_started": _lastSpeechAt = DateTime.UtcNow; break; diff --git a/client/session.py b/client/session.py index d52822c..deaf6fc 100644 --- a/client/session.py +++ b/client/session.py @@ -125,6 +125,8 @@ class Session: ) elif t == "config_updated": _log.info("config_updated: %s", env.get("config")) + elif t == "debug": + _log.info("backend %s", env.get("message")) else: _log.info("unhandled envelope type=%s", t)