From 0af3118d08f3078dec3c2c43c14488120088d275 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 14:29:42 +0000 Subject: [PATCH] fix: harden talk silence timeout parsing (#39607) (thanks @danodoesdesign) Co-authored-by: dano does design --- .secrets.baseline | 32 +++++++++---------- .../ai/openclaw/app/voice/TalkModeManager.kt | 4 ++- .../app/voice/TalkModeConfigParsingTest.kt | 7 ++++ apps/ios/Sources/Voice/TalkModeManager.swift | 5 ++- .../Tests/TalkModeConfigParsingTests.swift | 8 +++++ 5 files changed, 38 insertions(+), 18 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 2964cc29bda..697214ab1ec 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -9809,49 +9809,49 @@ "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "7f8aaf142ce0552c260f2e546dda43ddd7c9aef3", "is_verified": false, - "line_number": 1813 + "line_number": 1815 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "22af290a1a3d5e941193a41a3d3a9e4ca8da5e27", "is_verified": false, - "line_number": 1986 + "line_number": 1988 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "ec3810e10fb78db55ce38b9c18d1c3eb1db739e0", "is_verified": false, - "line_number": 2042 + "line_number": 2044 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "c1e6ee547fd492df1441ac492e8bb294974712bd", "is_verified": false, - "line_number": 2274 + "line_number": 2276 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "45d676e7c6ab44cf4b8fa366ef2d8fccd3e6d6e6", "is_verified": false, - "line_number": 2402 + "line_number": 2404 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "a219d7693c25cd2d93313512e200ff3eb374d281", "is_verified": false, - "line_number": 2655 + "line_number": 2657 }, { "type": "Secret Keyword", "filename": "docs/gateway/configuration-reference.md", "hashed_secret": "b6f56e5e92078ed7c078c46fbfeedcbe5719bc25", "is_verified": false, - "line_number": 2657 + "line_number": 2659 } ], "docs/gateway/configuration.md": [ @@ -10198,21 +10198,21 @@ "filename": "docs/tools/web.md", "hashed_secret": "6b26c117c66a0c030e239eef595c1e18865132a8", "is_verified": false, - "line_number": 90 + "line_number": 129 }, { "type": "Secret Keyword", "filename": "docs/tools/web.md", "hashed_secret": "491d458f895b9213facb2ee9375b1b044eaea3ac", "is_verified": false, - "line_number": 179 + "line_number": 202 }, { "type": "Secret Keyword", "filename": "docs/tools/web.md", "hashed_secret": "674397e2c0c2faaa85961c708d2a96a7cc7af217", "is_verified": false, - "line_number": 277 + "line_number": 303 } ], "docs/tts.md": [ @@ -11583,7 +11583,7 @@ "filename": "src/agents/pi-embedded-runner/model.ts", "hashed_secret": "e774aaeac31c6272107ba89080295e277050fa7c", "is_verified": false, - "line_number": 272 + "line_number": 267 } ], "src/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.ts": [ @@ -11680,7 +11680,7 @@ "filename": "src/agents/tools/web-search.ts", "hashed_secret": "dfba7aade0868074c2861c98e2a9a92f3178a51b", "is_verified": false, - "line_number": 254 + "line_number": 266 } ], "src/agents/tools/web-tools.enabled-defaults.e2e.test.ts": [ @@ -12335,14 +12335,14 @@ "filename": "src/config/schema.help.ts", "hashed_secret": "9f4cda226d3868676ac7f86f59e4190eb94bd208", "is_verified": false, - "line_number": 649 + "line_number": 651 }, { "type": "Secret Keyword", "filename": "src/config/schema.help.ts", "hashed_secret": "01822c8bbf6a8b136944b14182cb885100ec2eae", "is_verified": false, - "line_number": 680 + "line_number": 684 } ], "src/config/schema.irc.ts": [ @@ -12388,7 +12388,7 @@ "filename": "src/config/schema.labels.ts", "hashed_secret": "2eda7cd978f39eebec3bf03e4410a40e14167fff", "is_verified": false, - "line_number": 324 + "line_number": 325 } ], "src/config/slack-http-config.test.ts": [ @@ -13034,5 +13034,5 @@ } ] }, - "generated_at": "2026-03-08T13:52:40Z" + "generated_at": "2026-03-08T14:28:30Z" } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt index 31f8c20f010..8e17037f518 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt @@ -107,7 +107,9 @@ class TalkModeManager( } internal fun resolvedSilenceTimeoutMs(talk: JsonObject?): Long { - val timeout = talk?.get("silenceTimeoutMs").asDoubleOrNull() ?: return defaultSilenceTimeoutMs + val primitive = talk?.get("silenceTimeoutMs") as? JsonPrimitive ?: return defaultSilenceTimeoutMs + if (primitive.isString) return defaultSilenceTimeoutMs + val timeout = primitive.content.toDoubleOrNull() ?: return defaultSilenceTimeoutMs if (timeout <= 0 || timeout % 1.0 != 0.0 || timeout > Long.MAX_VALUE.toDouble()) { return defaultSilenceTimeoutMs } diff --git a/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt index bb9263140d9..a1a06e6aac3 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/voice/TalkModeConfigParsingTest.kt @@ -73,4 +73,11 @@ class TalkModeConfigParsingTest { assertEquals(700L, TalkModeManager.resolvedSilenceTimeoutMs(talk)) } + + @Test + fun defaultsSilenceTimeoutMsWhenString() { + val talk = buildJsonObject { put("silenceTimeoutMs", "1500") } + + assertEquals(700L, TalkModeManager.resolvedSilenceTimeoutMs(talk)) + } } diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index 3b9eb531b28..04172c14bfa 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -98,7 +98,7 @@ final class TalkModeManager: NSObject { private var gateway: GatewayNodeSession? private var gatewayConnected = false - private var silenceWindow: TimeInterval = TimeInterval(Self.defaultSilenceTimeoutMs) / 1000 + private var silenceWindow: TimeInterval = TimeInterval(TalkModeManager.defaultSilenceTimeoutMs) / 1000 private var lastAudioActivity: Date? private var noiseFloorSamples: [Double] = [] private var noiseFloor: Double? @@ -2010,6 +2010,9 @@ extension TalkModeManager { where timeout > 0 && timeout.rounded(.towardZero) == timeout && timeout <= Double(Int.max): return Int(timeout) case let timeout as NSNumber: + if CFGetTypeID(timeout) == CFBooleanGetTypeID() { + return Self.defaultSilenceTimeoutMs + } let value = timeout.doubleValue if value > 0 && value.rounded(.towardZero) == value && value <= Double(Int.max) { return Int(value) diff --git a/apps/ios/Tests/TalkModeConfigParsingTests.swift b/apps/ios/Tests/TalkModeConfigParsingTests.swift index b6d39df5d7b..6bf9bbef108 100644 --- a/apps/ios/Tests/TalkModeConfigParsingTests.swift +++ b/apps/ios/Tests/TalkModeConfigParsingTests.swift @@ -67,4 +67,12 @@ import Testing #expect(TalkModeManager.resolvedSilenceTimeoutMs(talk) == 900) } + + @Test func defaultsSilenceTimeoutMsWhenBool() { + let talk: [String: Any] = [ + "silenceTimeoutMs": true, + ] + + #expect(TalkModeManager.resolvedSilenceTimeoutMs(talk) == 900) + } }