Skip to content

Commit 3d16b7b

Browse files
chore: merge upstream/main and resolve CHANGELOG conflict
2 parents e0d32b2 + 87a0390 commit 3d16b7b

223 files changed

Lines changed: 5104 additions & 1314 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,16 @@ Docs: https://docs.openclaw.ai
110110
- Agents/exec: restore `host=node` routing for node-pinned and `host=auto` sessions, while still blocking sandboxed `auto` sessions from jumping to gateway. (#60788) Thanks @openperf.
111111
- Agents/compaction: keep assistant tool calls and displaced tool results in the same compaction chunk so strict summarization providers stop rejecting orphaned tool pairs. (#58849) Thanks @openperf.
112112
- Outbound/sanitizer: strip leaked `<tool_call>`, `<function_calls>`, and model special tokens from shared user-visible assistant text, including truncated tool-call streams, so internal scaffolding no longer bleeds into replies across surfaces. (#60619) Thanks @oliviareid-svg.
113+
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
114+
- Control UI/Overview: prevent gateway access token/password visibility toggle buttons from overlapping their inputs at narrow widths. (#56924) Thanks @bbddbb1.
115+
- Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.
116+
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
117+
- CLI/Commander: preserve Commander-computed exit codes for argument and help-error paths, and cover the user-argv parse mode in the regression tests so invalid CLI invocations no longer report success when exits are intercepted. (#60923) Thanks @Linux2010.
118+
- Telegram/native command menu: trim long menu descriptions before dropping commands so sub-100 command sets can still fit Telegram's payload budget and keep more `/` entries visible. (#61129) Thanks @neeravmakwana.
119+
- Agents/Claude CLI: keep non-interactive `--permission-mode bypassPermissions` when custom `cliBackends.claude-cli.args` override defaults, so cron and heartbeat Claude CLI runs do not regress to interactive approval mode. (#61114) Thanks @cathrynlavery and @thewilloftheshadow.
120+
- Agents/skills: skip `.git` and `node_modules` when mirroring skills into sandbox workspaces so read-only sandboxes do not copy repo history or dependency trees. (#61090) Thanks @joelnishanth.
121+
- Android/Talk Mode: cancel in-flight `talk.speak` playback when speech is explicitly stopped, so stale replies stop starting after barge-in or manual stop. (#61164) Thanks @obviyus.
122+
- Plugins/onboarding: write dotted plugin uiHint paths like Brave `webSearch.mode` as nested plugin config so `llm-context` setup stops failing validation. (#61159) Thanks @obviyus.
113123
- Media: fix `sanitizeMimeType` regex to anchor at end (`$`), enforce RFC 2045 `; attribute=value` parameter grammar, and apply case-insensitive matching per RFC 2045 §5.1, avoiding matches on trailing non-parameter suffixes while preserving valid parameterized MIME types. (#9795) Thanks @shamsulalam1114.
114124

115125
## 2026.4.2
@@ -207,6 +217,9 @@ Docs: https://docs.openclaw.ai
207217
- Browser/profiles: reject remote browser profile `cdpUrl` values that violate strict SSRF policy before saving config, with clearer validation errors for blocked endpoints. (#60477) Thanks @eleqtrizit.
208218
- Browser/screenshots: stop sending `fromSurface: false` on CDP screenshots so managed Chrome 146+ browsers can capture images again. (#60682) Thanks @mvanhorn.
209219
- Mattermost/slash commands: harden native slash-command callback token validation to use constant-time secret comparison, matching the existing interaction-token path.
220+
- Control UI/mobile chat: reduce narrow-screen overflow by shrinking the chat pane minimum width, removing extra mobile padding, widening message groups, and hiding avatars on very small screens. (#60220) Thanks @macdao.
221+
- Android/Talk Mode: route spoken replies through `talk.speak`, keep compressed playback cleanup deterministic, and fall back to local TTS for legacy gateways that omit Talk error reasons. (#60954) Thanks @obviyus.
222+
- Android/Talk Mode: keep reply-speaker routing and teardown behavior aligned with the new remote playback path. (#60954) Thanks @MKV21.
210223

211224
## 2026.4.1
212225

CONTRIBUTING.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@ Welcome to the lobster tank! 🦞
8585
4. **Test/CI-only PRs for known `main` failures** → Don't open a PR. The Maintainer team is already tracking those failures, and PRs that only tweak tests or CI to chase them will be closed unless they are required to validate a new fix.
8686
5. **Questions** → Discord [#help](https://discord.com/channels/1456350064065904867/1459642797895319552) / [#users-helping-users](https://discord.com/channels/1456350064065904867/1459007081603403828)
8787

88+
## PR Limits
89+
90+
We cap at **10 open PRs per author**. If you exceed this, the `r: too-many-prs` label is added and your PR is auto-closed. This is a hard limit.
91+
92+
For coordinated change sets that genuinely need more than 10 PRs, join the **#clawtributors** channel in Discord and talk to maintainers first.
93+
8894
## Before You PR
8995

9096
- Test locally with your OpenClaw instance

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

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,10 @@ class NodeRuntime(
381381
parseChatSendRunId(response) ?: idempotencyKey
382382
},
383383
speakAssistantReply = { text ->
384-
// Skip if TalkModeManager is handling TTS (ttsOnAllResponses) to avoid
385-
// double-speaking the same assistant reply from both pipelines.
386-
if (!talkMode.ttsOnAllResponses) {
387-
voiceReplySpeaker.speakAssistantReply(text)
388-
}
384+
// Voice-tab replies should speak through the dedicated reply speaker.
385+
// Relying on talkMode.ttsOnAllResponses here can drop playback if the
386+
// chat-event path misses the terminal event for this turn.
387+
voiceReplySpeaker.speakAssistantReply(text)
389388
},
390389
)
391390
}
@@ -596,12 +595,11 @@ class NodeRuntime(
596595

597596
scope.launch {
598597
prefs.talkEnabled.collect { enabled ->
599-
// MicCaptureManager handles STT + send to gateway.
600-
// TalkModeManager plays TTS on assistant responses.
598+
// MicCaptureManager handles STT + send to gateway, while the dedicated
599+
// reply speaker handles TTS for assistant replies in the voice tab.
601600
micCapture.setMicEnabled(enabled)
602601
if (enabled) {
603-
// Mic on = user is on voice screen and wants TTS responses.
604-
talkMode.ttsOnAllResponses = true
602+
talkMode.ttsOnAllResponses = false
605603
scope.launch { talkMode.ensureChatSubscribed() }
606604
}
607605
externalAudioCaptureActive.value = enabled
@@ -762,8 +760,8 @@ class NodeRuntime(
762760
prefs.setTalkEnabled(value)
763761
if (value) {
764762
// Tapping mic on interrupts any active TTS (barge-in)
765-
talkMode.stopTts()
766-
talkMode.ttsOnAllResponses = true
763+
stopVoicePlayback()
764+
talkMode.ttsOnAllResponses = false
767765
scope.launch { talkMode.ensureChatSubscribed() }
768766
}
769767
micCapture.setMicEnabled(value)
@@ -778,18 +776,25 @@ class NodeRuntime(
778776
if (voiceReplySpeakerLazy.isInitialized()) {
779777
voiceReplySpeaker.setPlaybackEnabled(value)
780778
}
781-
// Keep TalkMode in sync so speaker mute works when ttsOnAllResponses is active.
779+
// Keep TalkMode in sync so any active Talk playback also respects speaker mute.
782780
talkMode.setPlaybackEnabled(value)
783781
}
784782

785783
private fun stopActiveVoiceSession() {
786784
talkMode.ttsOnAllResponses = false
787-
talkMode.stopTts()
785+
stopVoicePlayback()
788786
micCapture.setMicEnabled(false)
789787
prefs.setTalkEnabled(false)
790788
externalAudioCaptureActive.value = false
791789
}
792790

791+
private fun stopVoicePlayback() {
792+
talkMode.stopTts()
793+
if (voiceReplySpeakerLazy.isInitialized()) {
794+
voiceReplySpeaker.stopTts()
795+
}
796+
}
797+
793798
fun refreshGatewayConnection() {
794799
val endpoint =
795800
connectedEndpoint ?: run {

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ data class GatewayConnectErrorDetails(
6464
val code: String?,
6565
val canRetryWithDeviceToken: Boolean,
6666
val recommendedNextStep: String?,
67+
val reason: String? = null,
6768
)
6869

6970
private data class SelectedConnectAuth(
@@ -116,6 +117,8 @@ class GatewaySession(
116117
val details: GatewayConnectErrorDetails? = null,
117118
)
118119

120+
data class RpcResult(val ok: Boolean, val payloadJson: String?, val error: ErrorShape?)
121+
119122
private val json = Json { ignoreUnknownKeys = true }
120123
private val writeLock = Mutex()
121124
private val pending = ConcurrentHashMap<String, CompletableDeferred<RpcResponse>>()
@@ -196,6 +199,13 @@ class GatewaySession(
196199
}
197200

198201
suspend fun request(method: String, paramsJson: String?, timeoutMs: Long = 15_000): String {
202+
val res = requestDetailed(method = method, paramsJson = paramsJson, timeoutMs = timeoutMs)
203+
if (res.ok) return res.payloadJson ?: ""
204+
val err = res.error
205+
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
206+
}
207+
208+
suspend fun requestDetailed(method: String, paramsJson: String?, timeoutMs: Long = 15_000): RpcResult {
199209
val conn = currentConnection ?: throw IllegalStateException("not connected")
200210
val params =
201211
if (paramsJson.isNullOrBlank()) {
@@ -204,9 +214,7 @@ class GatewaySession(
204214
json.parseToJsonElement(paramsJson)
205215
}
206216
val res = conn.request(method, params, timeoutMs)
207-
if (res.ok) return res.payloadJson ?: ""
208-
val err = res.error
209-
throw IllegalStateException("${err?.code ?: "UNAVAILABLE"}: ${err?.message ?: "request failed"}")
217+
return RpcResult(ok = res.ok, payloadJson = res.payloadJson, error = res.error)
210218
}
211219

212220
suspend fun refreshNodeCanvasCapability(timeoutMs: Long = 8_000): Boolean {
@@ -631,6 +639,7 @@ class GatewaySession(
631639
code = it["code"].asStringOrNull(),
632640
canRetryWithDeviceToken = it["canRetryWithDeviceToken"].asBooleanOrNull() == true,
633641
recommendedNextStep = it["recommendedNextStep"].asStringOrNull(),
642+
reason = it["reason"].asStringOrNull(),
634643
)
635644
}
636645
ErrorShape(code, msg, details)

apps/android/app/src/main/java/ai/openclaw/app/voice/MicCaptureManager.kt

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class MicCaptureManager(
9090
val isSending: StateFlow<Boolean> = _isSending
9191

9292
private val messageQueue = ArrayDeque<String>()
93+
private val messageQueueLock = Any()
9394
private var flushedPartialTranscript: String? = null
9495
private var pendingRunId: String? = null
9596
private var pendingAssistantEntryId: String? = null
@@ -105,6 +106,42 @@ class MicCaptureManager(
105106
private var ttsPauseDepth = 0
106107
private var resumeMicAfterTts = false
107108

109+
private fun enqueueMessage(message: String) {
110+
synchronized(messageQueueLock) {
111+
messageQueue.addLast(message)
112+
}
113+
}
114+
115+
private fun snapshotMessageQueue(): List<String> {
116+
return synchronized(messageQueueLock) {
117+
messageQueue.toList()
118+
}
119+
}
120+
121+
private fun hasQueuedMessages(): Boolean {
122+
return synchronized(messageQueueLock) {
123+
messageQueue.isNotEmpty()
124+
}
125+
}
126+
127+
private fun firstQueuedMessage(): String? {
128+
return synchronized(messageQueueLock) {
129+
messageQueue.firstOrNull()
130+
}
131+
}
132+
133+
private fun removeFirstQueuedMessage(): String? {
134+
return synchronized(messageQueueLock) {
135+
if (messageQueue.isEmpty()) null else messageQueue.removeFirst()
136+
}
137+
}
138+
139+
private fun queuedMessageCount(): Int {
140+
return synchronized(messageQueueLock) {
141+
messageQueue.size
142+
}
143+
}
144+
108145
fun setMicEnabled(enabled: Boolean) {
109146
if (_micEnabled.value == enabled) return
110147
_micEnabled.value = enabled
@@ -207,7 +244,7 @@ class MicCaptureManager(
207244
pendingRunId = null
208245
pendingAssistantEntryId = null
209246
_isSending.value = false
210-
if (messageQueue.isNotEmpty()) {
247+
if (hasQueuedMessages()) {
211248
_statusText.value = queuedWaitingStatus()
212249
}
213250
}
@@ -315,7 +352,7 @@ class MicCaptureManager(
315352
_statusText.value =
316353
when {
317354
_isSending.value -> "Listening · sending queued voice"
318-
messageQueue.isNotEmpty() -> "Listening · ${messageQueue.size} queued"
355+
hasQueuedMessages() -> "Listening · ${queuedMessageCount()} queued"
319356
else -> "Listening"
320357
}
321358
_isListening.value = true
@@ -348,7 +385,7 @@ class MicCaptureManager(
348385
role = VoiceConversationRole.User,
349386
text = message,
350387
)
351-
messageQueue.addLast(message)
388+
enqueueMessage(message)
352389
publishQueue()
353390
}
354391

@@ -367,12 +404,12 @@ class MicCaptureManager(
367404
}
368405

369406
private fun publishQueue() {
370-
_queuedMessages.value = messageQueue.toList()
407+
_queuedMessages.value = snapshotMessageQueue()
371408
}
372409

373410
private fun sendQueuedIfIdle() {
374411
if (_isSending.value) return
375-
if (messageQueue.isEmpty()) {
412+
if (!hasQueuedMessages()) {
376413
if (_micEnabled.value) {
377414
_statusText.value = "Listening"
378415
} else {
@@ -385,7 +422,7 @@ class MicCaptureManager(
385422
return
386423
}
387424

388-
val next = messageQueue.first()
425+
val next = firstQueuedMessage() ?: return
389426
_isSending.value = true
390427
pendingRunTimeoutJob?.cancel()
391428
pendingRunTimeoutJob = null
@@ -403,7 +440,7 @@ class MicCaptureManager(
403440
if (runId == null) {
404441
pendingRunTimeoutJob?.cancel()
405442
pendingRunTimeoutJob = null
406-
messageQueue.removeFirst()
443+
removeFirstQueuedMessage()
407444
publishQueue()
408445
_isSending.value = false
409446
pendingAssistantEntryId = null
@@ -449,8 +486,7 @@ class MicCaptureManager(
449486
private fun completePendingTurn() {
450487
pendingRunTimeoutJob?.cancel()
451488
pendingRunTimeoutJob = null
452-
if (messageQueue.isNotEmpty()) {
453-
messageQueue.removeFirst()
489+
if (removeFirstQueuedMessage() != null) {
454490
publishQueue()
455491
}
456492
pendingRunId = null
@@ -460,7 +496,7 @@ class MicCaptureManager(
460496
}
461497

462498
private fun queuedWaitingStatus(): String {
463-
return "${messageQueue.size} queued · waiting for gateway"
499+
return "${queuedMessageCount()} queued · waiting for gateway"
464500
}
465501

466502
private fun appendConversation(

0 commit comments

Comments
 (0)