Skip to content

Commit aaf5ab9

Browse files
steipetemorlutoZengWen-DTcxbAsDevNianJiuZst
authored
fix: land ten small reliability fixes (#100483)
* fix(agents): harden LSP process failures Source: #100450 Co-authored-by: morluto <[email protected]> * fix(sandbox): report effective workspace layout Sources: #100435, #100439 Co-authored-by: Aniruddha Adak <[email protected]> Co-authored-by: ZengWen-DT <[email protected]> * fix(security): fail install checks on stream errors Source: #100413 Co-authored-by: 陈宪彪0668000387 <[email protected]> * fix(android): normalize all-day calendar events Source: #100032 Co-authored-by: NianJiuZst <[email protected]> * fix(ios): serialize push-to-talk lifecycle Source: #99942 Co-authored-by: NianJiuZst <[email protected]> * fix(talk): reject inherited provider names Source: #99849 Co-authored-by: zenglingbiao <[email protected]> * fix(android): stop voice capture in background Source: #99840 Co-authored-by: xialonglee <[email protected]> * fix(cron): preserve fallback result classification Source: #99913 Co-authored-by: jincheng-xydt <[email protected]> * fix(google): bound Vertex response decompression Source: #99812 Co-authored-by: 黄剑雄0668001315 <[email protected]> * fix(plugins): report malformed discovery JSON Source: #99892 Co-authored-by: 陈宪彪0668000387 <[email protected]> * test(sandbox): configure non-default workspace fixture * test: fix small-fix batch validation --------- Co-authored-by: morluto <[email protected]> Co-authored-by: ZengWen-DT <[email protected]> Co-authored-by: 陈宪彪0668000387 <[email protected]> Co-authored-by: NianJiuZst <[email protected]> Co-authored-by: zenglingbiao <[email protected]> Co-authored-by: xialonglee <[email protected]> Co-authored-by: jincheng-xydt <[email protected]> Co-authored-by: 黄剑雄0668001315 <[email protected]>
1 parent 366ed56 commit aaf5ab9

34 files changed

Lines changed: 1195 additions & 131 deletions

apps/.i18n/native-source.json

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5587,55 +5587,55 @@
55875587
},
55885588
{
55895589
"kind": "conditional-branch",
5590-
"line": 340,
5590+
"line": 346,
55915591
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
55925592
"source": "Listening",
55935593
"surface": "android",
55945594
"id": "native.android.b4f67e96603515bd"
55955595
},
55965596
{
55975597
"kind": "conditional-branch",
5598-
"line": 340,
5598+
"line": 346,
55995599
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
56005600
"source": "Ready",
56015601
"surface": "android",
56025602
"id": "native.android.b88e4278ead724ba"
56035603
},
56045604
{
56055605
"kind": "conditional-branch",
5606-
"line": 700,
5606+
"line": 706,
56075607
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
56085608
"source": "Off",
56095609
"surface": "android",
56105610
"id": "native.android.8d8230088f3baa2a"
56115611
},
56125612
{
56135613
"kind": "conditional-branch",
5614-
"line": 701,
5614+
"line": 707,
56155615
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
56165616
"source": "Talk failed: Realtime provider closed unexpectedly.",
56175617
"surface": "android",
56185618
"id": "native.android.d6b7f86564ac3147"
56195619
},
56205620
{
56215621
"kind": "conditional-branch",
5622-
"line": 702,
5622+
"line": 708,
56235623
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
56245624
"source": "Talk failed: Realtime provider closed: $reason",
56255625
"surface": "android",
56265626
"id": "native.android.eb6ac739ad55e63a"
56275627
},
56285628
{
56295629
"kind": "conditional-branch",
5630-
"line": 1611,
5630+
"line": 1617,
56315631
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
56325632
"source": "Aborted",
56335633
"surface": "android",
56345634
"id": "native.android.13531fe081a45711"
56355635
},
56365636
{
56375637
"kind": "conditional-branch",
5638-
"line": 1611,
5638+
"line": 1617,
56395639
"path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt",
56405640
"source": "Chat error",
56415641
"surface": "android",
@@ -11051,63 +11051,63 @@
1105111051
},
1105211052
{
1105311053
"kind": "conditional-branch",
11054-
"line": 2545,
11054+
"line": 2603,
1105511055
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1105611056
"source": "Action required",
1105711057
"surface": "apple",
1105811058
"id": "native.apple.0d030e96f7738497"
1105911059
},
1106011060
{
1106111061
"kind": "conditional-branch",
11062-
"line": 2545,
11062+
"line": 2603,
1106311063
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1106411064
"source": "Approval needed",
1106511065
"surface": "apple",
1106611066
"id": "native.apple.ce6a022159f2fb41"
1106711067
},
1106811068
{
1106911069
"kind": "conditional-branch",
11070-
"line": 3100,
11070+
"line": 3158,
1107111071
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1107211072
"source": "Connecting…",
1107311073
"surface": "apple",
1107411074
"id": "native.apple.b0d9c1ba8ef7ec19"
1107511075
},
1107611076
{
1107711077
"kind": "conditional-branch",
11078-
"line": 3100,
11078+
"line": 3158,
1107911079
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1108011080
"source": "Reconnecting…",
1108111081
"surface": "apple",
1108211082
"id": "native.apple.bd98e19b49309284"
1108311083
},
1108411084
{
1108511085
"kind": "conditional-branch",
11086-
"line": 3104,
11086+
"line": 3162,
1108711087
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1108811088
"source": "Connecting...",
1108911089
"surface": "apple",
1109011090
"id": "native.apple.6504753f3621269e"
1109111091
},
1109211092
{
1109311093
"kind": "conditional-branch",
11094-
"line": 3104,
11094+
"line": 3162,
1109511095
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1109611096
"source": "Reconnecting...",
1109711097
"surface": "apple",
1109811098
"id": "native.apple.c7fbe2a713d78714"
1109911099
},
1110011100
{
1110111101
"kind": "conditional-branch",
11102-
"line": 3392,
11102+
"line": 3450,
1110311103
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1110411104
"source": "Connected",
1110511105
"surface": "apple",
1110611106
"id": "native.apple.c0a005f81d588867"
1110711107
},
1110811108
{
1110911109
"kind": "conditional-branch",
11110-
"line": 3392,
11110+
"line": 3450,
1111111111
"path": "apps/ios/Sources/Model/NodeAppModel.swift",
1111211112
"source": "Offline",
1111311113
"surface": "apple",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1378,7 +1378,7 @@ class NodeRuntime(
13781378
refreshExecApprovalsFromGateway()
13791379
}
13801380
} else {
1381-
stopManualVoiceSession()
1381+
stopActiveVoiceSession()
13821382
publishNodePresenceAliveBeacon(NodePresenceAliveBeacon.Trigger.Background, throttleRecentSuccess = true)
13831383
}
13841384
}
@@ -1877,7 +1877,7 @@ class NodeRuntime(
18771877

18781878
private fun stopActiveVoiceSession() {
18791879
talkMode.ttsOnAllResponses = false
1880-
talkMode.setEnabled(false)
1880+
talkMode.stopAllCapture()
18811881
stopVoicePlayback()
18821882
micCapture.setMicEnabled(false)
18831883
prefs.setVoiceMicEnabled(false)

apps/android/app/src/main/java/ai/openclaw/app/node/CalendarHandler.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,18 @@ internal data class CalendarAddRequest(
3737
val startMs: Long,
3838
val endMs: Long,
3939
val isAllDay: Boolean,
40+
val timeZoneId: String,
4041
val location: String?,
4142
val notes: String?,
4243
val calendarId: Long?,
4344
val calendarTitle: String?,
4445
)
4546

47+
private data class CalendarAddRange(
48+
val start: Instant,
49+
val end: Instant,
50+
)
51+
4652
/**
4753
* Normalized calendar event returned through gateway calendar commands.
4854
*/
@@ -148,7 +154,7 @@ private object SystemCalendarDataSource : CalendarDataSource {
148154
put(CalendarContract.Events.DTSTART, request.startMs)
149155
put(CalendarContract.Events.DTEND, request.endMs)
150156
put(CalendarContract.Events.ALL_DAY, if (request.isAllDay) 1 else 0)
151-
put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().id)
157+
put(CalendarContract.Events.EVENT_TIMEZONE, request.timeZoneId)
152158
request.location?.let { put(CalendarContract.Events.EVENT_LOCATION, it) }
153159
request.notes?.let { put(CalendarContract.Events.DESCRIPTION, it) }
154160
}
@@ -393,18 +399,35 @@ class CalendarHandler private constructor(
393399
val end =
394400
parseISO((params["endISO"] as? JsonPrimitive)?.content)
395401
?: return null
402+
val isAllDay = (params["isAllDay"] as? JsonPrimitive)?.content?.toBooleanStrictOrNull() ?: false
403+
val addRange = normalizeAddRange(start, end, isAllDay)
396404
return CalendarAddRequest(
397405
title = (params["title"] as? JsonPrimitive)?.content?.trim().orEmpty(),
398-
startMs = start.toEpochMilli(),
399-
endMs = end.toEpochMilli(),
400-
isAllDay = (params["isAllDay"] as? JsonPrimitive)?.content?.toBooleanStrictOrNull() ?: false,
406+
startMs = addRange.start.toEpochMilli(),
407+
endMs = addRange.end.toEpochMilli(),
408+
isAllDay = isAllDay,
409+
timeZoneId = if (isAllDay) "UTC" else TimeZone.getDefault().id,
401410
location = (params["location"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
402411
notes = (params["notes"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
403412
calendarId = (params["calendarId"] as? JsonPrimitive)?.content?.toLongOrNull(),
404413
calendarTitle = (params["calendarTitle"] as? JsonPrimitive)?.content?.trim()?.ifEmpty { null },
405414
)
406415
}
407416

417+
private fun normalizeAddRange(
418+
start: Instant,
419+
end: Instant,
420+
isAllDay: Boolean,
421+
): CalendarAddRange {
422+
if (!isAllDay || end <= start) return CalendarAddRange(start = start, end = end)
423+
val dayStart = start.truncatedTo(ChronoUnit.DAYS)
424+
val dayEnd = end.truncatedTo(ChronoUnit.DAYS)
425+
return CalendarAddRange(
426+
start = dayStart,
427+
end = if (dayEnd > dayStart) dayEnd else dayStart.plus(1, ChronoUnit.DAYS),
428+
)
429+
}
430+
408431
private fun parseISO(raw: String?): Instant? {
409432
val value = raw?.trim().orEmpty()
410433
if (value.isEmpty()) return null

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,12 @@ class TalkModeManager internal constructor(
240240
}
241241
}
242242

243+
/** Stops continuous, one-shot, or push-to-talk capture regardless of the enabled flag. */
244+
fun stopAllCapture() {
245+
_isEnabled.value = false
246+
stop()
247+
}
248+
243249
/** Starts a push-to-talk capture session for gateway node.invoke callers. */
244250
suspend fun beginPushToTalk(allowNewCapture: Boolean): TalkPttStartPayload {
245251
if (!allowNewCapture) {

apps/android/app/src/test/java/ai/openclaw/app/GatewayBootstrapAuthTest.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,46 @@ class GatewayBootstrapAuthTest {
666666
assertFalse(readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value)
667667
}
668668

669+
@Test
670+
fun backgroundingStopsTalkModeCapture() {
671+
val app = RuntimeEnvironment.getApplication()
672+
val runtime = NodeRuntime(app)
673+
val talkMode = readField<Lazy<TalkModeManager>>(runtime, "talkMode\$delegate").value
674+
readField<MutableStateFlow<VoiceCaptureMode>>(runtime, "_voiceCaptureMode").value = VoiceCaptureMode.TalkMode
675+
readField<MutableStateFlow<Boolean>>(talkMode, "_isEnabled").value = true
676+
readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value = true
677+
talkMode.ttsOnAllResponses = true
678+
679+
assertEquals(VoiceCaptureMode.TalkMode, runtime.voiceCaptureMode.value)
680+
assertTrue(talkMode.isEnabled.value)
681+
assertTrue(readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value)
682+
683+
runtime.setForeground(false)
684+
685+
assertEquals(VoiceCaptureMode.Off, runtime.voiceCaptureMode.value)
686+
assertFalse(talkMode.isEnabled.value)
687+
assertFalse(talkMode.ttsOnAllResponses)
688+
assertFalse(readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value)
689+
}
690+
691+
@Test
692+
fun backgroundingStopsGatewayPttWhenVoiceModeIsOff() {
693+
val app = RuntimeEnvironment.getApplication()
694+
shadowOf(app).grantPermissions(Manifest.permission.RECORD_AUDIO)
695+
val runtime = NodeRuntime(app)
696+
val talkMode = readField<Lazy<TalkModeManager>>(runtime, "talkMode\$delegate").value
697+
writeField(talkMode, "activePttCaptureId", "capture-1")
698+
readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value = true
699+
700+
assertEquals(VoiceCaptureMode.Off, runtime.voiceCaptureMode.value)
701+
702+
runtime.setForeground(false)
703+
704+
assertNull(readField<String?>(talkMode, "activePttCaptureId"))
705+
assertEquals(VoiceCaptureMode.Off, runtime.voiceCaptureMode.value)
706+
assertFalse(readField<MutableStateFlow<Boolean>>(runtime, "externalAudioCaptureActive").value)
707+
}
708+
669709
private fun waitForGatewayTrustPrompt(runtime: NodeRuntime): NodeRuntime.GatewayTrustPrompt {
670710
repeat(50) {
671711
runtime.pendingGatewayTrust.value?.let { return it }

apps/android/app/src/test/java/ai/openclaw/app/node/CalendarHandlerTest.kt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import org.junit.Assert.assertEquals
99
import org.junit.Assert.assertFalse
1010
import org.junit.Assert.assertTrue
1111
import org.junit.Test
12+
import java.time.Instant
13+
import java.util.TimeZone
1214

1315
class CalendarHandlerTest : NodeHandlerRobolectricTest() {
1416
@Test
@@ -86,6 +88,58 @@ class CalendarHandlerTest : NodeHandlerRobolectricTest() {
8688
assertFalse(result.ok)
8789
assertEquals("CALENDAR_NOT_FOUND", result.error?.code)
8890
}
91+
92+
@Test
93+
fun handleCalendarAdd_normalizesAllDayEventForAndroidProvider() {
94+
val source = FakeCalendarDataSource(canRead = true, canWrite = true)
95+
val handler = CalendarHandler.forTesting(appContext(), source)
96+
97+
val result =
98+
handler.handleCalendarAdd(
99+
"""{"title":"Holiday","startISO":"2026-07-05T09:00:00Z","endISO":"2026-07-06T09:00:00Z","isAllDay":true}""",
100+
)
101+
102+
assertTrue(result.ok)
103+
val request = source.addedRequest ?: error("missing add request")
104+
assertTrue(request.isAllDay)
105+
assertEquals("UTC", request.timeZoneId)
106+
assertEquals(Instant.parse("2026-07-05T00:00:00Z").toEpochMilli(), request.startMs)
107+
assertEquals(Instant.parse("2026-07-06T00:00:00Z").toEpochMilli(), request.endMs)
108+
}
109+
110+
@Test
111+
fun handleCalendarAdd_expandsSameDayAllDayRangeToOneDay() {
112+
val source = FakeCalendarDataSource(canRead = true, canWrite = true)
113+
val handler = CalendarHandler.forTesting(appContext(), source)
114+
115+
val result =
116+
handler.handleCalendarAdd(
117+
"""{"title":"Holiday","startISO":"2026-07-05T09:00:00Z","endISO":"2026-07-05T17:00:00Z","isAllDay":true}""",
118+
)
119+
120+
assertTrue(result.ok)
121+
val request = source.addedRequest ?: error("missing add request")
122+
assertEquals(Instant.parse("2026-07-05T00:00:00Z").toEpochMilli(), request.startMs)
123+
assertEquals(Instant.parse("2026-07-06T00:00:00Z").toEpochMilli(), request.endMs)
124+
}
125+
126+
@Test
127+
fun handleCalendarAdd_preservesTimedInstantsAndDeviceTimezone() {
128+
val source = FakeCalendarDataSource(canRead = true, canWrite = true)
129+
val handler = CalendarHandler.forTesting(appContext(), source)
130+
131+
val result =
132+
handler.handleCalendarAdd(
133+
"""{"title":"Call","startISO":"2026-07-05T09:15:00Z","endISO":"2026-07-05T10:45:00Z"}""",
134+
)
135+
136+
assertTrue(result.ok)
137+
val request = source.addedRequest ?: error("missing add request")
138+
assertFalse(request.isAllDay)
139+
assertEquals(TimeZone.getDefault().id, request.timeZoneId)
140+
assertEquals(Instant.parse("2026-07-05T09:15:00Z").toEpochMilli(), request.startMs)
141+
assertEquals(Instant.parse("2026-07-05T10:45:00Z").toEpochMilli(), request.endMs)
142+
}
89143
}
90144

91145
private class FakeCalendarDataSource(
@@ -104,6 +158,9 @@ private class FakeCalendarDataSource(
104158
),
105159
private val addError: Throwable? = null,
106160
) : CalendarDataSource {
161+
var addedRequest: CalendarAddRequest? = null
162+
private set
163+
107164
override fun hasReadPermission(context: Context): Boolean = canRead
108165

109166
override fun hasWritePermission(context: Context): Boolean = canWrite
@@ -118,6 +175,7 @@ private class FakeCalendarDataSource(
118175
request: CalendarAddRequest,
119176
): CalendarEventRecord {
120177
addError?.let { throw it }
178+
addedRequest = request
121179
return addResult
122180
}
123181
}

apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeManagerTest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import kotlinx.coroutines.test.currentTime
1818
import kotlinx.coroutines.test.runTest
1919
import org.junit.Assert.assertEquals
2020
import org.junit.Assert.assertFalse
21+
import org.junit.Assert.assertNull
2122
import org.junit.Assert.assertTrue
2223
import org.junit.Test
2324
import org.junit.runner.RunWith
@@ -82,6 +83,20 @@ class TalkModeManagerTest {
8283
assertEquals("capture-1", payload.captureId)
8384
}
8485

86+
@Test
87+
fun stopAllCaptureClearsPttWhenContinuousModeIsDisabled() {
88+
val manager = createManager()
89+
setPrivateField(manager, "activePttCaptureId", "capture-1")
90+
setMutableStateFlow(manager, "_isListening", true)
91+
92+
manager.stopAllCapture()
93+
94+
assertNull(readPrivateField(manager, "activePttCaptureId"))
95+
assertFalse(manager.isEnabled.value)
96+
assertFalse(manager.isListening.value)
97+
assertEquals("Off", manager.statusText.value)
98+
}
99+
85100
@Test
86101
fun duplicateFinalForPendingTalkRunDoesNotStartAllResponseTts() {
87102
val manager = createManager()

0 commit comments

Comments
 (0)