Skip to content

Commit 683ad75

Browse files
authored
fix(talk): stabilize realtime voice consults
Stabilize realtime Talk playback, transcript ordering, and consult routing across Android, Web, and the gateway relay. - serialize Android realtime playback and transcript updates - add opt-in forced consult routing for Talk realtime sessions - keep web/gateway consult turns behind OpenClaw results with ordered transcript bubbles - document the new `talk.realtime.consultRouting` config and keep prompt wording generic Co-authored-by: VACInc <[email protected]>
1 parent 29118a0 commit 683ad75

38 files changed

Lines changed: 2228 additions & 114 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ Docs: https://docs.openclaw.ai
212212
- Cron: keep recovered tool warnings diagnostic for successful scheduled runs so final cron output is delivered instead of being replaced by a post-processing warning. (#84045) Thanks @abnershang.
213213
- Plugins/perf: thread explicit plugin discovery results through `loadBundledCapabilityRuntimeRegistry`, `resolveBundledPluginSources`, and `listChannelCatalogEntries` so callers that already hold a discovery result skip redundant filesystem walks. Thanks @SebTardif.
214214
- harden update restart script creation [AI]. (#84088) Thanks @pgondhi987.
215+
- Android/Control UI Talk: split realtime voice transcript turns, queue PCM playback writes, and add opt-in OpenClaw consult routing for Gateway relay when a realtime provider skips `openclaw_agent_consult`. (#84181) Thanks @VACInc.
215216
- Docker: keep the bundled Codex plugin in official release image keep lists so the default OpenAI agent harness remains available after Docker pruning. Fixes #83613. (#83626) Thanks @YuanHanzhong.
216217
- CLI/channels: preserve the first line of `openclaw channels logs` output when the rolling tail window starts exactly on a line boundary, mirroring the already-fixed `readLogSlice` behavior in `src/logging/log-tail.ts`.
217218
- Control UI: treat terminal session status as authoritative over stale active-run flags so completed terminal runs stop showing abort/live UI. (#84057)

apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package ai.openclaw.app.gateway
22

33
import android.util.Log
4+
import kotlinx.coroutines.CancellationException
45
import kotlinx.coroutines.CompletableDeferred
56
import kotlinx.coroutines.CoroutineScope
67
import kotlinx.coroutines.Dispatchers
78
import kotlinx.coroutines.Job
89
import kotlinx.coroutines.TimeoutCancellationException
910
import kotlinx.coroutines.cancelAndJoin
11+
import kotlinx.coroutines.channels.Channel
1012
import kotlinx.coroutines.delay
1113
import kotlinx.coroutines.isActive
1214
import kotlinx.coroutines.launch
@@ -384,6 +386,22 @@ class GatewaySession(
384386
private val client: OkHttpClient = buildClient()
385387
private var socket: WebSocket? = null
386388
private val loggerTag = "OpenClawGateway"
389+
private val incomingMessages = Channel<String>(Channel.UNLIMITED)
390+
private val messagePumpJob =
391+
scope.launch(Dispatchers.IO) {
392+
for (text in incomingMessages) {
393+
try {
394+
handleMessage(text)
395+
} catch (err: CancellationException) {
396+
throw err
397+
} catch (err: Throwable) {
398+
Log.w(
399+
loggerTag,
400+
"gateway message handling failed: ${err.message ?: err::class.java.simpleName}",
401+
)
402+
}
403+
}
404+
}
387405

388406
val remoteAddress: String = formatGatewayAuthority(endpoint.host, endpoint.port)
389407

@@ -475,6 +493,11 @@ class GatewaySession(
475493

476494
fun closeQuietly() {
477495
if (isClosed.compareAndSet(false, true)) {
496+
incomingMessages.close()
497+
messagePumpJob.cancel()
498+
if (!connectDeferred.isCompleted) {
499+
connectDeferred.completeExceptionally(IllegalStateException("Gateway closed"))
500+
}
478501
socket?.close(1000, "bye")
479502
socket = null
480503
closedDeferred.complete(Unit)
@@ -519,7 +542,7 @@ class GatewaySession(
519542
webSocket: WebSocket,
520543
text: String,
521544
) {
522-
scope.launch { handleMessage(text) }
545+
incomingMessages.trySend(text)
523546
}
524547

525548
override fun onFailure(
@@ -531,6 +554,7 @@ class GatewaySession(
531554
connectDeferred.completeExceptionally(t)
532555
}
533556
if (isClosed.compareAndSet(false, true)) {
557+
incomingMessages.close()
534558
failPending()
535559
closedDeferred.complete(Unit)
536560
onDisconnected("Gateway error: ${t.message ?: t::class.java.simpleName}")
@@ -546,6 +570,7 @@ class GatewaySession(
546570
connectDeferred.completeExceptionally(IllegalStateException("Gateway closed: $reason"))
547571
}
548572
if (isClosed.compareAndSet(false, true)) {
573+
incomingMessages.close()
549574
failPending()
550575
closedDeferred.complete(Unit)
551576
onDisconnected("Gateway closed: $reason")

0 commit comments

Comments
 (0)