Skip to content

Commit 34a5c47

Browse files
committed
fix: preserve Android assistant auto-send queue
1 parent 462b402 commit 34a5c47

6 files changed

Lines changed: 136 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
1212

1313
- Agents/tools: include value-shape hints in missing-parameter tool errors so dropped, empty-string, and wrong-type write payloads are easier to diagnose from logs. (#55317) Thanks @priyansh19.
1414
- Plugins/browser: block SSRF redirect bypass by installing a real-time Playwright route handler before `page.goto()` so navigation to private/internal IPs is intercepted and aborted mid-redirect instead of checked post-hoc. (#58771) Thanks @pgondhi987.
15+
- Android/assistant: keep queued App Actions prompts pending when auto-send enqueue is rejected, so transient chat-health drops do not silently lose the assistant request. Thanks @obviyus.
1516

1617
## 2026.4.2-beta.1
1718

apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,4 +366,16 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
366366
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
367367
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
368368
}
369+
370+
suspend fun sendChatAwaitAcceptance(
371+
message: String,
372+
thinking: String,
373+
attachments: List<OutgoingAttachment>,
374+
): Boolean {
375+
return ensureRuntime().sendChatAwaitAcceptance(
376+
message = message,
377+
thinking = thinking,
378+
attachments = attachments,
379+
)
380+
}
369381
}

apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,14 @@ class NodeRuntime(
10161016
chat.sendMessage(message = message, thinkingLevel = thinking, attachments = attachments)
10171017
}
10181018

1019+
suspend fun sendChatAwaitAcceptance(
1020+
message: String,
1021+
thinking: String,
1022+
attachments: List<OutgoingAttachment>,
1023+
): Boolean {
1024+
return chat.sendMessageAwaitAcceptance(message = message, thinkingLevel = thinking, attachments = attachments)
1025+
}
1026+
10191027
private fun handleGatewayEvent(event: String, payloadJson: String?) {
10201028
micCapture.handleGatewayEvent(event, payloadJson)
10211029
talkMode.handleGatewayEvent(event, payloadJson)

apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,25 @@ class ChatController(
130130
thinkingLevel: String,
131131
attachments: List<OutgoingAttachment>,
132132
) {
133+
scope.launch {
134+
sendMessageAwaitAcceptance(
135+
message = message,
136+
thinkingLevel = thinkingLevel,
137+
attachments = attachments,
138+
)
139+
}
140+
}
141+
142+
suspend fun sendMessageAwaitAcceptance(
143+
message: String,
144+
thinkingLevel: String,
145+
attachments: List<OutgoingAttachment>,
146+
): Boolean {
133147
val trimmed = message.trim()
134-
if (trimmed.isEmpty() && attachments.isEmpty()) return
148+
if (trimmed.isEmpty() && attachments.isEmpty()) return false
135149
if (!_healthOk.value) {
136150
_errorText.value = "Gateway health not OK; cannot send"
137-
return
151+
return false
138152
}
139153

140154
val runId = UUID.randomUUID().toString()
@@ -177,45 +191,45 @@ class ChatController(
177191
pendingToolCallsById.clear()
178192
publishPendingToolCalls()
179193

180-
scope.launch {
181-
try {
182-
val params =
183-
buildJsonObject {
184-
put("sessionKey", JsonPrimitive(sessionKey))
185-
put("message", JsonPrimitive(text))
186-
put("thinking", JsonPrimitive(thinking))
187-
put("timeoutMs", JsonPrimitive(30_000))
188-
put("idempotencyKey", JsonPrimitive(runId))
189-
if (attachments.isNotEmpty()) {
190-
put(
191-
"attachments",
192-
JsonArray(
193-
attachments.map { att ->
194-
buildJsonObject {
195-
put("type", JsonPrimitive(att.type))
196-
put("mimeType", JsonPrimitive(att.mimeType))
197-
put("fileName", JsonPrimitive(att.fileName))
198-
put("content", JsonPrimitive(att.base64))
199-
}
200-
},
201-
),
202-
)
203-
}
204-
}
205-
val res = session.request("chat.send", params.toString())
206-
val actualRunId = parseRunId(res) ?: runId
207-
if (actualRunId != runId) {
208-
clearPendingRun(runId)
209-
armPendingRunTimeout(actualRunId)
210-
synchronized(pendingRuns) {
211-
pendingRuns.add(actualRunId)
212-
_pendingRunCount.value = pendingRuns.size
194+
return try {
195+
val params =
196+
buildJsonObject {
197+
put("sessionKey", JsonPrimitive(sessionKey))
198+
put("message", JsonPrimitive(text))
199+
put("thinking", JsonPrimitive(thinking))
200+
put("timeoutMs", JsonPrimitive(30_000))
201+
put("idempotencyKey", JsonPrimitive(runId))
202+
if (attachments.isNotEmpty()) {
203+
put(
204+
"attachments",
205+
JsonArray(
206+
attachments.map { att ->
207+
buildJsonObject {
208+
put("type", JsonPrimitive(att.type))
209+
put("mimeType", JsonPrimitive(att.mimeType))
210+
put("fileName", JsonPrimitive(att.fileName))
211+
put("content", JsonPrimitive(att.base64))
212+
}
213+
},
214+
),
215+
)
213216
}
214217
}
215-
} catch (err: Throwable) {
218+
val res = session.request("chat.send", params.toString())
219+
val actualRunId = parseRunId(res) ?: runId
220+
if (actualRunId != runId) {
216221
clearPendingRun(runId)
217-
_errorText.value = err.message
222+
armPendingRunTimeout(actualRunId)
223+
synchronized(pendingRuns) {
224+
pendingRuns.add(actualRunId)
225+
_pendingRunCount.value = pendingRuns.size
226+
}
218227
}
228+
true
229+
} catch (err: Throwable) {
230+
clearPendingRun(runId)
231+
_errorText.value = err.message
232+
false
219233
}
220234
}
221235

apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatSheetContent.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,21 @@ internal fun resolvePendingAssistantAutoSend(
5858
return prompt
5959
}
6060

61+
internal suspend fun dispatchPendingAssistantAutoSend(
62+
pendingPrompt: String?,
63+
healthOk: Boolean,
64+
pendingRunCount: Int,
65+
dispatch: suspend (String) -> Boolean,
66+
): Boolean {
67+
val prompt =
68+
resolvePendingAssistantAutoSend(
69+
pendingPrompt = pendingPrompt,
70+
healthOk = healthOk,
71+
pendingRunCount = pendingRunCount,
72+
) ?: return false
73+
return dispatch(prompt)
74+
}
75+
6176
@Composable
6277
fun ChatSheetContent(viewModel: MainViewModel) {
6378
val messages by viewModel.chatMessages.collectAsState()
@@ -78,13 +93,19 @@ fun ChatSheetContent(viewModel: MainViewModel) {
7893
}
7994

8095
LaunchedEffect(pendingAssistantAutoSend, healthOk, pendingRunCount, thinkingLevel) {
81-
val prompt =
82-
resolvePendingAssistantAutoSend(
96+
val accepted =
97+
dispatchPendingAssistantAutoSend(
8398
pendingPrompt = pendingAssistantAutoSend,
8499
healthOk = healthOk,
85100
pendingRunCount = pendingRunCount,
86-
) ?: return@LaunchedEffect
87-
viewModel.sendChat(message = prompt, thinking = thinkingLevel, attachments = emptyList())
101+
) { prompt ->
102+
viewModel.sendChatAwaitAcceptance(
103+
message = prompt,
104+
thinking = thinkingLevel,
105+
attachments = emptyList(),
106+
)
107+
}
108+
if (!accepted) return@LaunchedEffect
88109
viewModel.clearPendingAssistantAutoSend()
89110
}
90111

apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatSheetContentTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package ai.openclaw.app.ui.chat
22

33
import org.junit.Assert.assertEquals
44
import org.junit.Assert.assertNull
5+
import org.junit.Assert.assertFalse
6+
import org.junit.Assert.assertTrue
57
import org.junit.Test
8+
import kotlinx.coroutines.runBlocking
69

710
class ChatSheetContentTest {
811
@Test
@@ -30,4 +33,40 @@ class ChatSheetContentTest {
3033
),
3134
)
3235
}
36+
37+
@Test
38+
fun keepsPendingAssistantAutoSendWhenDispatchRejected() = runBlocking {
39+
var dispatchedPrompt: String? = null
40+
41+
val consumed =
42+
dispatchPendingAssistantAutoSend(
43+
pendingPrompt = "summarize mail",
44+
healthOk = true,
45+
pendingRunCount = 0,
46+
) { prompt ->
47+
dispatchedPrompt = prompt
48+
false
49+
}
50+
51+
assertFalse(consumed)
52+
assertEquals("summarize mail", dispatchedPrompt)
53+
}
54+
55+
@Test
56+
fun clearsPendingAssistantAutoSendOnlyAfterAcceptedDispatch() = runBlocking {
57+
var dispatchedPrompt: String? = null
58+
59+
val consumed =
60+
dispatchPendingAssistantAutoSend(
61+
pendingPrompt = "summarize mail",
62+
healthOk = true,
63+
pendingRunCount = 0,
64+
) { prompt ->
65+
dispatchedPrompt = prompt
66+
true
67+
}
68+
69+
assertTrue(consumed)
70+
assertEquals("summarize mail", dispatchedPrompt)
71+
}
3372
}

0 commit comments

Comments
 (0)