diff --git a/backend/DeviceHub/DeviceHubEndpoint.cs b/backend/DeviceHub/DeviceHubEndpoint.cs index be57004..5d655f6 100644 --- a/backend/DeviceHub/DeviceHubEndpoint.cs +++ b/backend/DeviceHub/DeviceHubEndpoint.cs @@ -177,6 +177,10 @@ public static class DeviceHubEndpoint }, ct); } + // Bump on every backend change so we can verify a Coolify deploy landed + // by looking at the hello_ack config.server_version field in the Pi journal. + public const string ServerVersion = "plan3-debug-2026-06-12-09"; + private static async Task SendHelloAckAsync(ActiveDevice active, Device device, CancellationToken ct) { var enabled = device.Config?.EnabledToolsJson ?? "[]"; @@ -186,7 +190,8 @@ public static class DeviceHubEndpoint "model": "{{Escape(device.Config?.Model)}}", "system_prompt": "{{Escape(device.Config?.SystemPrompt)}}", "idle_timeout_seconds": {{device.Config?.IdleTimeoutSeconds ?? 30}}, - "enabled_tools": {{enabled}} + "enabled_tools": {{enabled}}, + "server_version": "{{ServerVersion}}" } """)!.AsObject(); diff --git a/backend/Realtime/RealtimeEvents.cs b/backend/Realtime/RealtimeEvents.cs index fbe6fef..ea9b0f2 100644 --- a/backend/Realtime/RealtimeEvents.cs +++ b/backend/Realtime/RealtimeEvents.cs @@ -26,6 +26,11 @@ public static class RealtimeEvents }); } + // Minimal GA session.update. We drop explicit audio.input/output.format + // objects (the GA shape accepts them but OpenAI defaults to 24 kHz PCM16 + // mono on both directions, which is what we use). Removing the explicit + // format objects matches the documented minimal session config and + // eliminates a known source of OpenAI silently ignoring the audio. return new JsonObject { ["type"] = "session.update", @@ -38,11 +43,6 @@ public static class RealtimeEvents { ["input"] = new JsonObject { - ["format"] = new JsonObject - { - ["type"] = "audio/pcm", - ["rate"] = 24000, - }, ["transcription"] = new JsonObject { ["model"] = "whisper-1" }, ["turn_detection"] = new JsonObject { @@ -50,9 +50,6 @@ public static class RealtimeEvents ["threshold"] = 0.5, ["prefix_padding_ms"] = 300, ["silence_duration_ms"] = 500, - // GA requires both explicitly — without them the - // model never auto-generates a reply after VAD - // detects speech_stopped. ["create_response"] = true, ["interrupt_response"] = true, }, @@ -60,11 +57,6 @@ public static class RealtimeEvents ["output"] = new JsonObject { ["voice"] = cfg.Voice, - ["format"] = new JsonObject - { - ["type"] = "audio/pcm", - ["rate"] = 24000, - }, }, }, ["tools"] = tools, diff --git a/backend/Realtime/RealtimeSession.cs b/backend/Realtime/RealtimeSession.cs index 61ee09e..9586dd1 100644 --- a/backend/Realtime/RealtimeSession.cs +++ b/backend/Realtime/RealtimeSession.cs @@ -128,10 +128,18 @@ public class RealtimeSession if (evt is null && ct.IsCancellationRequested) return; 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. + // Upstream has gone silent. Surface that to the device every + // ~2s so we can tell "OpenAI is up but emitting nothing" from + // "the upstream WS was closed" — Coolify logs aren't reachable. if (++nullPolls % 8 == 0) - _logger.LogDebug("upstream silent ({Polls} x 250ms polls)", nullPolls); + { + _logger.LogInformation("upstream silent ({Polls} x 250ms polls)", nullPolls); + await _output.WriteEnvelopeAsync(new JsonObject + { + ["type"] = "debug", + ["message"] = "upstream silent " + nullPolls + "x250ms", + }, ct); + } continue; } nullPolls = 0;