From 754494d1a0ad63969c46fa61d74f447772d0c560 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 19 Jan 2026 16:53:31 +0000 Subject: [PATCH] fix(android): align node protocol payloads --- CHANGELOG.md | 1 + .../java/com/clawdbot/android/NodeRuntime.kt | 9 +++++ .../android/gateway/GatewaySession.kt | 37 +++++++++++++++++-- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9b6c18ef..afe6d23f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.clawd.bot ### Changes - Android: remove legacy bridge transport code now that nodes use the gateway protocol. +- Android: send structured payloads in node events/invokes and include user-agent metadata in gateway connects. ## 2026.1.19-2 diff --git a/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt b/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt index 302fff989..d132b7ff4 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt @@ -498,6 +498,13 @@ class NodeRuntime(context: Context) { .ifEmpty { null } } + private fun buildUserAgent(): String { + val version = resolvedVersionName() + val release = Build.VERSION.RELEASE?.trim().orEmpty() + val releaseLabel = if (release.isEmpty()) "unknown" else release + return "ClawdbotAndroid/$version (Android $releaseLabel; SDK ${Build.VERSION.SDK_INT})" + } + private fun buildClientInfo(clientId: String, clientMode: String): GatewayClientInfo { return GatewayClientInfo( id = clientId, @@ -519,6 +526,7 @@ class NodeRuntime(context: Context) { commands = buildInvokeCommands(), permissions = emptyMap(), client = buildClientInfo(clientId = "node-host", clientMode = "node"), + userAgent = buildUserAgent(), ) } @@ -530,6 +538,7 @@ class NodeRuntime(context: Context) { commands = emptyList(), permissions = emptyMap(), client = buildClientInfo(clientId = "clawdbot-control-ui", clientMode = "ui"), + userAgent = buildUserAgent(), ) } diff --git a/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt index cf80066de..cf4d4a81d 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/gateway/GatewaySession.kt @@ -49,6 +49,7 @@ data class GatewayConnectOptions( val commands: List, val permissions: Map, val client: GatewayClientInfo, + val userAgent: String? = null, ) class GatewaySession( @@ -131,10 +132,17 @@ class GatewaySession( suspend fun sendNodeEvent(event: String, payloadJson: String?) { val conn = currentConnection ?: return + val parsedPayload = payloadJson?.let { parseJsonOrNull(it) } val params = buildJsonObject { put("event", JsonPrimitive(event)) - if (payloadJson != null) put("payloadJSON", JsonPrimitive(payloadJson)) else put("payloadJSON", JsonNull) + if (parsedPayload != null) { + put("payload", parsedPayload) + } else if (payloadJson != null) { + put("payloadJSON", JsonPrimitive(payloadJson)) + } else { + put("payloadJSON", JsonNull) + } } try { conn.request("node.event", params, timeoutMs = 8_000) @@ -377,6 +385,9 @@ class GatewaySession( authJson?.let { put("auth", it) } deviceJson?.let { put("device", it) } put("locale", JsonPrimitive(locale)) + options.userAgent?.trim()?.takeIf { it.isNotEmpty() }?.let { + put("userAgent", JsonPrimitive(it)) + } } } @@ -403,7 +414,8 @@ class GatewaySession( private fun handleEvent(frame: JsonObject) { val event = frame["event"].asStringOrNull() ?: return - val payloadJson = frame["payload"]?.let { it.toString() } + val payloadJson = + frame["payload"]?.let { it.toString() } ?: frame["payloadJSON"].asStringOrNull() if (event == "node.invoke.request" && payloadJson != null && onInvoke != null) { handleInvokeEvent(payloadJson) return @@ -421,7 +433,9 @@ class GatewaySession( val id = payload["id"].asStringOrNull() ?: return val nodeId = payload["nodeId"].asStringOrNull() ?: return val command = payload["command"].asStringOrNull() ?: return - val params = payload["paramsJSON"].asStringOrNull() + val params = + payload["paramsJSON"].asStringOrNull() + ?: payload["params"]?.let { value -> if (value is JsonNull) null else value.toString() } val timeoutMs = payload["timeoutMs"].asLongOrNull() scope.launch { val result = @@ -436,12 +450,17 @@ class GatewaySession( } private suspend fun sendInvokeResult(id: String, nodeId: String, result: InvokeResult) { + val parsedPayload = result.payloadJson?.let { parseJsonOrNull(it) } val params = buildJsonObject { put("id", JsonPrimitive(id)) put("nodeId", JsonPrimitive(nodeId)) put("ok", JsonPrimitive(result.ok)) - if (result.payloadJson != null) put("payloadJSON", JsonPrimitive(result.payloadJson)) + if (parsedPayload != null) { + put("payload", parsedPayload) + } else if (result.payloadJson != null) { + put("payloadJSON", JsonPrimitive(result.payloadJson)) + } result.error?.let { err -> put( "error", @@ -599,3 +618,13 @@ private fun JsonElement?.asLongOrNull(): Long? = is JsonPrimitive -> content.toLongOrNull() else -> null } + +private fun parseJsonOrNull(payload: String): JsonElement? { + val trimmed = payload.trim() + if (trimmed.isEmpty()) return null + return try { + Json.parseToJsonElement(trimmed) + } catch (_: Throwable) { + null + } +}