From 34486f8c10e2cb4bbfa9d75a05a9eddbe01dff80 Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Fri, 27 Feb 2026 11:49:31 +0530 Subject: [PATCH] fix(android): retry A2UI after canvas capability refresh --- .../android/gateway/GatewaySession.kt | 50 +++++++++++++++++++ .../openclaw/android/node/InvokeDispatcher.kt | 21 ++++++-- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt index 5262899f09b..18c2bf21368 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/gateway/GatewaySession.kt @@ -173,6 +173,43 @@ class GatewaySession( throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}") } + suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean { + val conn = currentConnection ?: return false + val response = + try { + conn.request("node.canvas.capability.refresh", params = null, timeoutMs = timeoutMs) + } catch (err: Throwable) { + Log.w("OpenClawGateway", "node.canvas.capability.refresh failed: ${err.message ?: err::class.java.simpleName}") + return false + } + if (!response.ok) { + val err = response.error + Log.w( + "OpenClawGateway", + "node.canvas.capability.refresh rejected: ${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}", + ) + return false + } + val payloadObj = response.payloadJson?.let(::parseJsonOrNull)?.asObjectOrNull() + val refreshedCapability = payloadObj?.get("canvasCapability").asStringOrNull()?.trim().orEmpty() + if (refreshedCapability.isEmpty()) { + Log.w("OpenClawGateway", "node.canvas.capability.refresh missing canvasCapability") + return false + } + val scopedCanvasHostUrl = canvasHostUrl?.trim().orEmpty() + if (scopedCanvasHostUrl.isEmpty()) { + Log.w("OpenClawGateway", "node.canvas.capability.refresh missing local canvasHostUrl") + return false + } + val refreshedUrl = replaceCanvasCapabilityInScopedHostUrl(scopedCanvasHostUrl, refreshedCapability) + if (refreshedUrl == null) { + Log.w("OpenClawGateway", "node.canvas.capability.refresh unable to rewrite scoped canvas URL") + return false + } + canvasHostUrl = refreshedUrl + return true + } + private data class RpcResponse(val id: String, val ok: Boolean, val payloadJson: String?, val error: ErrorShape?) private inner class Connection( @@ -697,6 +734,19 @@ private fun parseJsonOrNull(payload: String): JsonElement? { } } +private fun replaceCanvasCapabilityInScopedHostUrl( + scopedUrl: String, + capability: String, +): String? { + val marker = "/__openclaw__/cap/" + val markerStart = scopedUrl.indexOf(marker) + if (markerStart < 0) return null + val capabilityStart = markerStart + marker.length + val capabilityEnd = scopedUrl.indexOf("/", capabilityStart) + if (capabilityEnd <= capabilityStart) return null + return scopedUrl.substring(0, capabilityStart) + capability + scopedUrl.substring(capabilityEnd) +} + internal fun resolveInvokeResultAckTimeoutMs(invokeTimeoutMs: Long?): Long { val normalized = invokeTimeoutMs?.takeIf { it > 0L } ?: 15_000L return normalized.coerceIn(15_000L, 120_000L) diff --git a/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeDispatcher.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeDispatcher.kt index 365ca8ea999..7a57f314f9e 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeDispatcher.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeDispatcher.kt @@ -26,6 +26,7 @@ class InvokeDispatcher( private val locationEnabled: () -> Boolean, private val smsAvailable: () -> Boolean, private val debugBuild: () -> Boolean, + private val refreshNodeCanvasCapability: suspend () -> Boolean, private val onCanvasA2uiPush: () -> Unit, private val onCanvasA2uiReset: () -> Unit, ) { @@ -149,16 +150,28 @@ class InvokeDispatcher( private suspend fun withReadyA2ui( block: suspend () -> GatewaySession.InvokeResult, ): GatewaySession.InvokeResult { - val a2uiUrl = a2uiHandler.resolveA2uiHostUrl() + var a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: return GatewaySession.InvokeResult.error( code = "A2UI_HOST_NOT_CONFIGURED", message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", ) - val ready = a2uiHandler.ensureA2uiReady(a2uiUrl) - if (!ready) { + if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) { + if (!refreshNodeCanvasCapability()) { + return GatewaySession.InvokeResult.error( + code = "A2UI_HOST_UNAVAILABLE", + message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable", + ) + } + a2uiUrl = a2uiHandler.resolveA2uiHostUrl() + ?: return GatewaySession.InvokeResult.error( + code = "A2UI_HOST_NOT_CONFIGURED", + message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host", + ) + } + if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) { return GatewaySession.InvokeResult.error( code = "A2UI_HOST_UNAVAILABLE", - message = "A2UI host not reachable", + message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable", ) } return block()