mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 13:13:06 +00:00
fix: preserve Android assistant auto-send queue
This commit is contained in:
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
## 2026.4.2-beta.1
|
||||
|
||||
|
||||
@@ -366,4 +366,16 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
|
||||
fun sendChat(message: String, thinking: String, attachments: List<OutgoingAttachment>) {
|
||||
ensureRuntime().sendChat(message = message, thinking = thinking, attachments = attachments)
|
||||
}
|
||||
|
||||
suspend fun sendChatAwaitAcceptance(
|
||||
message: String,
|
||||
thinking: String,
|
||||
attachments: List<OutgoingAttachment>,
|
||||
): Boolean {
|
||||
return ensureRuntime().sendChatAwaitAcceptance(
|
||||
message = message,
|
||||
thinking = thinking,
|
||||
attachments = attachments,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1016,6 +1016,14 @@ class NodeRuntime(
|
||||
chat.sendMessage(message = message, thinkingLevel = thinking, attachments = attachments)
|
||||
}
|
||||
|
||||
suspend fun sendChatAwaitAcceptance(
|
||||
message: String,
|
||||
thinking: String,
|
||||
attachments: List<OutgoingAttachment>,
|
||||
): Boolean {
|
||||
return chat.sendMessageAwaitAcceptance(message = message, thinkingLevel = thinking, attachments = attachments)
|
||||
}
|
||||
|
||||
private fun handleGatewayEvent(event: String, payloadJson: String?) {
|
||||
micCapture.handleGatewayEvent(event, payloadJson)
|
||||
talkMode.handleGatewayEvent(event, payloadJson)
|
||||
|
||||
@@ -130,11 +130,25 @@ class ChatController(
|
||||
thinkingLevel: String,
|
||||
attachments: List<OutgoingAttachment>,
|
||||
) {
|
||||
scope.launch {
|
||||
sendMessageAwaitAcceptance(
|
||||
message = message,
|
||||
thinkingLevel = thinkingLevel,
|
||||
attachments = attachments,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendMessageAwaitAcceptance(
|
||||
message: String,
|
||||
thinkingLevel: String,
|
||||
attachments: List<OutgoingAttachment>,
|
||||
): Boolean {
|
||||
val trimmed = message.trim()
|
||||
if (trimmed.isEmpty() && attachments.isEmpty()) return
|
||||
if (trimmed.isEmpty() && attachments.isEmpty()) return false
|
||||
if (!_healthOk.value) {
|
||||
_errorText.value = "Gateway health not OK; cannot send"
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
val runId = UUID.randomUUID().toString()
|
||||
@@ -177,45 +191,45 @@ class ChatController(
|
||||
pendingToolCallsById.clear()
|
||||
publishPendingToolCalls()
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val params =
|
||||
buildJsonObject {
|
||||
put("sessionKey", JsonPrimitive(sessionKey))
|
||||
put("message", JsonPrimitive(text))
|
||||
put("thinking", JsonPrimitive(thinking))
|
||||
put("timeoutMs", JsonPrimitive(30_000))
|
||||
put("idempotencyKey", JsonPrimitive(runId))
|
||||
if (attachments.isNotEmpty()) {
|
||||
put(
|
||||
"attachments",
|
||||
JsonArray(
|
||||
attachments.map { att ->
|
||||
buildJsonObject {
|
||||
put("type", JsonPrimitive(att.type))
|
||||
put("mimeType", JsonPrimitive(att.mimeType))
|
||||
put("fileName", JsonPrimitive(att.fileName))
|
||||
put("content", JsonPrimitive(att.base64))
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
val res = session.request("chat.send", params.toString())
|
||||
val actualRunId = parseRunId(res) ?: runId
|
||||
if (actualRunId != runId) {
|
||||
clearPendingRun(runId)
|
||||
armPendingRunTimeout(actualRunId)
|
||||
synchronized(pendingRuns) {
|
||||
pendingRuns.add(actualRunId)
|
||||
_pendingRunCount.value = pendingRuns.size
|
||||
return try {
|
||||
val params =
|
||||
buildJsonObject {
|
||||
put("sessionKey", JsonPrimitive(sessionKey))
|
||||
put("message", JsonPrimitive(text))
|
||||
put("thinking", JsonPrimitive(thinking))
|
||||
put("timeoutMs", JsonPrimitive(30_000))
|
||||
put("idempotencyKey", JsonPrimitive(runId))
|
||||
if (attachments.isNotEmpty()) {
|
||||
put(
|
||||
"attachments",
|
||||
JsonArray(
|
||||
attachments.map { att ->
|
||||
buildJsonObject {
|
||||
put("type", JsonPrimitive(att.type))
|
||||
put("mimeType", JsonPrimitive(att.mimeType))
|
||||
put("fileName", JsonPrimitive(att.fileName))
|
||||
put("content", JsonPrimitive(att.base64))
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (err: Throwable) {
|
||||
val res = session.request("chat.send", params.toString())
|
||||
val actualRunId = parseRunId(res) ?: runId
|
||||
if (actualRunId != runId) {
|
||||
clearPendingRun(runId)
|
||||
_errorText.value = err.message
|
||||
armPendingRunTimeout(actualRunId)
|
||||
synchronized(pendingRuns) {
|
||||
pendingRuns.add(actualRunId)
|
||||
_pendingRunCount.value = pendingRuns.size
|
||||
}
|
||||
}
|
||||
true
|
||||
} catch (err: Throwable) {
|
||||
clearPendingRun(runId)
|
||||
_errorText.value = err.message
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,21 @@ internal fun resolvePendingAssistantAutoSend(
|
||||
return prompt
|
||||
}
|
||||
|
||||
internal suspend fun dispatchPendingAssistantAutoSend(
|
||||
pendingPrompt: String?,
|
||||
healthOk: Boolean,
|
||||
pendingRunCount: Int,
|
||||
dispatch: suspend (String) -> Boolean,
|
||||
): Boolean {
|
||||
val prompt =
|
||||
resolvePendingAssistantAutoSend(
|
||||
pendingPrompt = pendingPrompt,
|
||||
healthOk = healthOk,
|
||||
pendingRunCount = pendingRunCount,
|
||||
) ?: return false
|
||||
return dispatch(prompt)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatSheetContent(viewModel: MainViewModel) {
|
||||
val messages by viewModel.chatMessages.collectAsState()
|
||||
@@ -78,13 +93,19 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
||||
}
|
||||
|
||||
LaunchedEffect(pendingAssistantAutoSend, healthOk, pendingRunCount, thinkingLevel) {
|
||||
val prompt =
|
||||
resolvePendingAssistantAutoSend(
|
||||
val accepted =
|
||||
dispatchPendingAssistantAutoSend(
|
||||
pendingPrompt = pendingAssistantAutoSend,
|
||||
healthOk = healthOk,
|
||||
pendingRunCount = pendingRunCount,
|
||||
) ?: return@LaunchedEffect
|
||||
viewModel.sendChat(message = prompt, thinking = thinkingLevel, attachments = emptyList())
|
||||
) { prompt ->
|
||||
viewModel.sendChatAwaitAcceptance(
|
||||
message = prompt,
|
||||
thinking = thinkingLevel,
|
||||
attachments = emptyList(),
|
||||
)
|
||||
}
|
||||
if (!accepted) return@LaunchedEffect
|
||||
viewModel.clearPendingAssistantAutoSend()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package ai.openclaw.app.ui.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class ChatSheetContentTest {
|
||||
@Test
|
||||
@@ -30,4 +33,40 @@ class ChatSheetContentTest {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keepsPendingAssistantAutoSendWhenDispatchRejected() = runBlocking {
|
||||
var dispatchedPrompt: String? = null
|
||||
|
||||
val consumed =
|
||||
dispatchPendingAssistantAutoSend(
|
||||
pendingPrompt = "summarize mail",
|
||||
healthOk = true,
|
||||
pendingRunCount = 0,
|
||||
) { prompt ->
|
||||
dispatchedPrompt = prompt
|
||||
false
|
||||
}
|
||||
|
||||
assertFalse(consumed)
|
||||
assertEquals("summarize mail", dispatchedPrompt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun clearsPendingAssistantAutoSendOnlyAfterAcceptedDispatch() = runBlocking {
|
||||
var dispatchedPrompt: String? = null
|
||||
|
||||
val consumed =
|
||||
dispatchPendingAssistantAutoSend(
|
||||
pendingPrompt = "summarize mail",
|
||||
healthOk = true,
|
||||
pendingRunCount = 0,
|
||||
) { prompt ->
|
||||
dispatchedPrompt = prompt
|
||||
true
|
||||
}
|
||||
|
||||
assertTrue(consumed)
|
||||
assertEquals("summarize mail", dispatchedPrompt)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user