mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 02:06:43 +00:00
fix(ios): harden Talk voice-session lifecycle from review (#111424)
- Close+clear the adopted logical voice session when a config/provider change invalidates a realtime prefetch (a live call is preserved); prevents the gateway rejecting a stale voiceSessionId resume under a new provider. - Retry talk.client.close (0/500/2000ms) before forgetting the session, matching the web client, so a transient close does not orphan it until the 6h sweep. - Surface exhausted transcript-persist failures through the session delegate so a dropped affirmation transcript no longer silently blocks spoken confirmation. - Localize the "Confirmation needed" status through the presentation switches; add its native locale translations.
This commit is contained in:
@@ -1281,6 +1281,38 @@
|
||||
"指令"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "ar",
|
||||
"source": "Confirmation needed",
|
||||
"translations": [
|
||||
"التأكيد مطلوب",
|
||||
"يلزم التأكيد"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "fa",
|
||||
"source": "Confirmation needed",
|
||||
"translations": [
|
||||
"تأیید لازم است",
|
||||
"نیاز به تأیید"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "th",
|
||||
"source": "Confirmation needed",
|
||||
"translations": [
|
||||
"ต้องการการยืนยัน",
|
||||
"ต้องยืนยัน"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "tr",
|
||||
"source": "Confirmation needed",
|
||||
"translations": [
|
||||
"Onay gerekiyor",
|
||||
"Onay gerekli"
|
||||
]
|
||||
},
|
||||
{
|
||||
"locale": "fr",
|
||||
"source": "Connect",
|
||||
|
||||
@@ -25763,7 +25763,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 611,
|
||||
"line": 614,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Gateway relay ready",
|
||||
"surface": "apple",
|
||||
@@ -25771,7 +25771,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 613,
|
||||
"line": 616,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Realtime Voice, Gateway relay ready",
|
||||
"surface": "apple",
|
||||
@@ -25779,7 +25779,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 615,
|
||||
"line": 618,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Listening starts from this phone",
|
||||
"surface": "apple",
|
||||
@@ -25787,7 +25787,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 861,
|
||||
"line": 864,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Off",
|
||||
"surface": "apple",
|
||||
@@ -25795,7 +25795,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 864,
|
||||
"line": 867,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Not active",
|
||||
"surface": "apple",
|
||||
@@ -25803,7 +25803,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1052,
|
||||
"line": 1055,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Requesting permissions…",
|
||||
"surface": "apple",
|
||||
@@ -25811,7 +25811,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1081,
|
||||
"line": 1084,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Start failed: %@",
|
||||
"surface": "apple",
|
||||
@@ -25819,7 +25819,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1337,
|
||||
"line": 1340,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Microphone permission denied",
|
||||
"surface": "apple",
|
||||
@@ -25827,7 +25827,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1347,
|
||||
"line": 1350,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Speech recognition",
|
||||
"surface": "apple",
|
||||
@@ -25835,7 +25835,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1478,
|
||||
"line": 1481,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Offline",
|
||||
"surface": "apple",
|
||||
@@ -25843,7 +25843,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1677,
|
||||
"line": 1680,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Speech error: %@",
|
||||
"surface": "apple",
|
||||
@@ -25851,7 +25851,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 1992,
|
||||
"line": 1995,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Gateway not connected",
|
||||
"surface": "apple",
|
||||
@@ -25859,7 +25859,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 2036,
|
||||
"line": 2039,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Aborted",
|
||||
"surface": "apple",
|
||||
@@ -25867,7 +25867,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 2036,
|
||||
"line": 2039,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Chat error",
|
||||
"surface": "apple",
|
||||
@@ -25875,7 +25875,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 2064,
|
||||
"line": 2067,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Talk failed: %@",
|
||||
"surface": "apple",
|
||||
@@ -25883,7 +25883,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 2168,
|
||||
"line": 2171,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "No reply",
|
||||
"surface": "apple",
|
||||
@@ -25891,7 +25891,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 2340,
|
||||
"line": 2343,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Paused",
|
||||
"surface": "apple",
|
||||
@@ -25899,7 +25899,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 2869,
|
||||
"line": 2929,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Generating voice…",
|
||||
"surface": "apple",
|
||||
@@ -25907,7 +25907,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 3007,
|
||||
"line": 3067,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Speak failed: %@",
|
||||
"surface": "apple",
|
||||
@@ -25915,7 +25915,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 3106,
|
||||
"line": 3166,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Speaking (System)…",
|
||||
"surface": "apple",
|
||||
@@ -25923,7 +25923,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 3987,
|
||||
"line": 4047,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "iOS System Voice",
|
||||
"surface": "apple",
|
||||
@@ -25931,7 +25931,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 3989,
|
||||
"line": 4049,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Realtime Voice",
|
||||
"surface": "apple",
|
||||
@@ -25939,7 +25939,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 3993,
|
||||
"line": 4053,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Gateway Default",
|
||||
"surface": "apple",
|
||||
@@ -25947,7 +25947,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4052,
|
||||
"line": 4114,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Listening (Realtime)",
|
||||
"surface": "apple",
|
||||
@@ -25955,7 +25955,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4054,
|
||||
"line": 4116,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Thinking",
|
||||
"surface": "apple",
|
||||
@@ -25963,7 +25963,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4056,
|
||||
"line": 4118,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Thinking…",
|
||||
"surface": "apple",
|
||||
@@ -25971,7 +25971,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4058,
|
||||
"line": 4120,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Asking OpenClaw",
|
||||
"surface": "apple",
|
||||
@@ -25979,7 +25979,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4060,
|
||||
"line": 4122,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Still asking OpenClaw",
|
||||
"surface": "apple",
|
||||
@@ -25987,7 +25987,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4062,
|
||||
"line": 4124,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Updating OpenClaw",
|
||||
"surface": "apple",
|
||||
@@ -25995,7 +25995,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4064,
|
||||
"line": 4126,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Speaking",
|
||||
"surface": "apple",
|
||||
@@ -26003,7 +26003,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4066,
|
||||
"line": 4128,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Speaking…",
|
||||
"surface": "apple",
|
||||
@@ -26011,7 +26011,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4068,
|
||||
"line": 4130,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Connecting",
|
||||
"surface": "apple",
|
||||
@@ -26019,7 +26019,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4070,
|
||||
"line": 4132,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Connecting realtime…",
|
||||
"surface": "apple",
|
||||
@@ -26027,7 +26027,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4072,
|
||||
"line": 4134,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Waiting for realtime…",
|
||||
"surface": "apple",
|
||||
@@ -26035,7 +26035,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4074,
|
||||
"line": 4136,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Ready",
|
||||
"surface": "apple",
|
||||
@@ -26043,7 +26043,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4076,
|
||||
"line": 4138,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Reconnecting",
|
||||
"surface": "apple",
|
||||
@@ -26051,7 +26051,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4078,
|
||||
"line": 4140,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Reconnecting…",
|
||||
"surface": "apple",
|
||||
@@ -26059,7 +26059,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4080,
|
||||
"line": 4142,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Realtime failed before connecting",
|
||||
"surface": "apple",
|
||||
@@ -26067,7 +26067,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4082,
|
||||
"line": 4144,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Realtime disconnected",
|
||||
"surface": "apple",
|
||||
@@ -26075,7 +26075,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4084,
|
||||
"line": 4146,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "OpenClaw unavailable",
|
||||
"surface": "apple",
|
||||
@@ -26083,7 +26083,15 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4124,
|
||||
"line": 4148,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Confirmation needed",
|
||||
"surface": "apple",
|
||||
"id": "native.apple.f4adf2762402e92a"
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4188,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "iOS Speech + TTS",
|
||||
"surface": "apple",
|
||||
@@ -26091,7 +26099,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4126,
|
||||
"line": 4190,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Listening",
|
||||
"surface": "apple",
|
||||
@@ -26099,7 +26107,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4130,
|
||||
"line": 4194,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "iOS Speech fallback",
|
||||
"surface": "apple",
|
||||
@@ -26107,7 +26115,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4391,
|
||||
"line": 4455,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Gateway Relay",
|
||||
"surface": "apple",
|
||||
@@ -26115,7 +26123,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4393,
|
||||
"line": 4457,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Native WebRTC",
|
||||
"surface": "apple",
|
||||
@@ -26123,7 +26131,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4394,
|
||||
"line": 4458,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Native",
|
||||
"surface": "apple",
|
||||
@@ -26131,7 +26139,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4455,
|
||||
"line": 4519,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Gateway permission required",
|
||||
"surface": "apple",
|
||||
@@ -26139,7 +26147,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4477,
|
||||
"line": 4541,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Not loaded",
|
||||
"surface": "apple",
|
||||
@@ -26147,7 +26155,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4495,
|
||||
"line": 4559,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Approval requested",
|
||||
"surface": "apple",
|
||||
@@ -26155,7 +26163,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4934,
|
||||
"line": 5079,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Realtime unavailable",
|
||||
"surface": "apple",
|
||||
@@ -26163,7 +26171,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "ui-localized-call",
|
||||
"line": 4956,
|
||||
"line": 5101,
|
||||
"path": "apps/ios/Sources/Voice/TalkModeManager.swift",
|
||||
"source": "Listening (PTT)",
|
||||
"surface": "apple",
|
||||
@@ -26171,7 +26179,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 616,
|
||||
"line": 634,
|
||||
"path": "apps/ios/Sources/Voice/TalkRealtimeWebRTCSession.swift",
|
||||
"source": "Asking OpenClaw",
|
||||
"surface": "apple",
|
||||
@@ -26179,7 +26187,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 616,
|
||||
"line": 634,
|
||||
"path": "apps/ios/Sources/Voice/TalkRealtimeWebRTCSession.swift",
|
||||
"source": "Updating OpenClaw",
|
||||
"surface": "apple",
|
||||
@@ -26187,7 +26195,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 705,
|
||||
"line": 723,
|
||||
"path": "apps/ios/Sources/Voice/TalkRealtimeWebRTCSession.swift",
|
||||
"source": "Confirmation needed",
|
||||
"surface": "apple",
|
||||
@@ -26195,7 +26203,7 @@
|
||||
},
|
||||
{
|
||||
"kind": "conditional-branch",
|
||||
"line": 705,
|
||||
"line": 723,
|
||||
"path": "apps/ios/Sources/Voice/TalkRealtimeWebRTCSession.swift",
|
||||
"source": "OpenClaw unavailable",
|
||||
"surface": "apple",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw غير متاح"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "يلزم التأكيد"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw nicht verfügbar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Bestätigung erforderlich"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw no está disponible"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Se necesita confirmación"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw در دسترس نیست"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "تأیید لازم است"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw indisponible"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Confirmation requise"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw उपलब्ध नहीं है"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "पुष्टि आवश्यक है"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw tidak tersedia"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Konfirmasi diperlukan"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw non disponibile"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Conferma necessaria"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClawは利用できません"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "確認が必要です"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw를 사용할 수 없습니다"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "확인이 필요합니다"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw niet beschikbaar"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Bevestiging vereist"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw jest niedostępny"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Wymagane potwierdzenie"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw indisponível"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Confirmação necessária"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw недоступен"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Требуется подтверждение"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw är inte tillgängligt"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Bekräftelse krävs"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw ไม่พร้อมใช้งาน"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "ต้องการการยืนยัน"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw kullanılamıyor"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Onay gerekiyor"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw недоступний"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Потрібне підтвердження"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw không khả dụng"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "Cần xác nhận"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw 不可用"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "需要确认"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -16303,6 +16303,11 @@
|
||||
"source": "OpenClaw unavailable",
|
||||
"translated": "OpenClaw 無法使用"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f4adf2762402e92a",
|
||||
"source": "Confirmation needed",
|
||||
"translated": "需要確認"
|
||||
},
|
||||
{
|
||||
"id": "native.apple.f703391677039fa3",
|
||||
"source": "iOS Speech + TTS",
|
||||
|
||||
@@ -208,6 +208,7 @@ final class TalkModeManager: NSObject {
|
||||
|
||||
private static let realtimeStableSessionSeconds: TimeInterval = 30
|
||||
private static let realtimeRestartDelaysNanoseconds: [UInt64] = [500_000_000, 2_000_000_000]
|
||||
private static let realtimeVoiceSessionCloseRetryDelaysNanoseconds: [UInt64] = [0, 500_000_000, 2_000_000_000]
|
||||
|
||||
private var isStarting = false
|
||||
private var startAttemptID = 0
|
||||
@@ -335,6 +336,8 @@ final class TalkModeManager: NSObject {
|
||||
@ObservationIgnored private var testPTTFinalizerHandler: (@MainActor () async -> Void)?
|
||||
@ObservationIgnored private var testPTTOnceStartedHandler: (@MainActor () async -> Void)?
|
||||
@ObservationIgnored private var testPTTReservedHandler: (@MainActor () async -> Void)?
|
||||
@ObservationIgnored private var testRealtimeVoiceSessionCloseRequest:
|
||||
(@MainActor (_ method: String, _ paramsJSON: String?) async throws -> Void)?
|
||||
#endif
|
||||
|
||||
private let logger = Logger(subsystem: "ai.openclawfoundation.app", category: "TalkMode")
|
||||
@@ -545,7 +548,7 @@ final class TalkModeManager: NSObject {
|
||||
self.gatewayTalkActiveModeTitle = String(localized: "Not active")
|
||||
self.gatewayTalkActiveModeSubtitle = nil
|
||||
self.cancelRealtimePrefetch()
|
||||
self.prefetchedRealtimeSession = nil
|
||||
self.invalidatePrefetchedRealtimeSession()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2591,7 +2594,19 @@ final class TalkModeManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func closeLogicalRealtimeVoiceSessions() {
|
||||
@discardableResult
|
||||
private func invalidatePrefetchedRealtimeSession() -> Task<Void, Never>? {
|
||||
guard self.realtimeSession == nil else {
|
||||
// A config reload may overlap a live direct call. Discard only an unused prefetch;
|
||||
// the active transport still owns its logical session and transcript queue.
|
||||
self.prefetchedRealtimeSession = nil
|
||||
return nil
|
||||
}
|
||||
return self.closeLogicalRealtimeVoiceSessions()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func closeLogicalRealtimeVoiceSessions() -> Task<Void, Never>? {
|
||||
// A close boundary invalidates every in-flight transport that captured the old owner.
|
||||
self.realtimeVoiceSessionGeneration &+= 1
|
||||
let voiceSessionIds = Set([
|
||||
@@ -2601,11 +2616,11 @@ final class TalkModeManager: NSObject {
|
||||
].compactMap(\.self))
|
||||
self.activeRealtimeVoiceSessionId = nil
|
||||
self.prefetchedRealtimeSession = nil
|
||||
guard !voiceSessionIds.isEmpty else { return }
|
||||
guard !voiceSessionIds.isEmpty else { return nil }
|
||||
let gateway = self.gateway
|
||||
let sessionKey = self.mainSessionKey
|
||||
let transcriptStore = self.realtimeTranscriptStore
|
||||
Task { @MainActor in
|
||||
return Task { @MainActor in
|
||||
defer { transcriptStore.remove(voiceSessionIds) }
|
||||
for voiceSessionId in voiceSessionIds.sorted() {
|
||||
await transcriptStore.flush(voiceSessionId: voiceSessionId)
|
||||
@@ -2617,7 +2632,7 @@ final class TalkModeManager: NSObject {
|
||||
continue
|
||||
}
|
||||
do {
|
||||
try await Self.closeRealtimeVoiceSession(
|
||||
try await self.closeRealtimeVoiceSession(
|
||||
gateway: gateway,
|
||||
sessionKey: sessionKey,
|
||||
voiceSessionId: voiceSessionId)
|
||||
@@ -2630,7 +2645,46 @@ final class TalkModeManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
private static func closeRealtimeVoiceSession(
|
||||
private static func retryRealtimeVoiceSessionClose(
|
||||
retryDelaysNanoseconds: [UInt64],
|
||||
sleep: @escaping @MainActor (UInt64) async throws -> Void = { delay in
|
||||
try await Task.sleep(nanoseconds: delay)
|
||||
},
|
||||
operation: @escaping @MainActor () async throws -> Void) async throws
|
||||
{
|
||||
var finalError: Error?
|
||||
for delay in retryDelaysNanoseconds {
|
||||
if delay > 0 {
|
||||
try await sleep(delay)
|
||||
}
|
||||
do {
|
||||
try await operation()
|
||||
return
|
||||
} catch {
|
||||
finalError = error
|
||||
}
|
||||
}
|
||||
throw finalError ?? NSError(domain: "TalkRealtimeVoiceSession", code: 2, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Voice session close failed",
|
||||
])
|
||||
}
|
||||
|
||||
private func closeRealtimeVoiceSession(
|
||||
gateway: GatewayNodeSession,
|
||||
sessionKey: String,
|
||||
voiceSessionId: String) async throws
|
||||
{
|
||||
try await Self.retryRealtimeVoiceSessionClose(
|
||||
retryDelaysNanoseconds: Self.realtimeVoiceSessionCloseRetryDelaysNanoseconds)
|
||||
{
|
||||
try await self.requestRealtimeVoiceSessionClose(
|
||||
gateway: gateway,
|
||||
sessionKey: sessionKey,
|
||||
voiceSessionId: voiceSessionId)
|
||||
}
|
||||
}
|
||||
|
||||
private func requestRealtimeVoiceSessionClose(
|
||||
gateway: GatewayNodeSession,
|
||||
sessionKey: String,
|
||||
voiceSessionId: String) async throws
|
||||
@@ -2644,6 +2698,12 @@ final class TalkModeManager: NSObject {
|
||||
NSLocalizedDescriptionKey: "Failed to encode close request",
|
||||
])
|
||||
}
|
||||
#if DEBUG
|
||||
if let testRealtimeVoiceSessionCloseRequest {
|
||||
try await testRealtimeVoiceSessionCloseRequest("talk.client.close", json)
|
||||
return
|
||||
}
|
||||
#endif
|
||||
_ = try await gateway.request(
|
||||
method: "talk.client.close",
|
||||
paramsJSON: json,
|
||||
@@ -2660,7 +2720,7 @@ final class TalkModeManager: NSObject {
|
||||
defer { transcriptStore.remove([voiceSessionId]) }
|
||||
await transcriptStore.flush(voiceSessionId: voiceSessionId)
|
||||
do {
|
||||
try await Self.closeRealtimeVoiceSession(
|
||||
try await self.closeRealtimeVoiceSession(
|
||||
gateway: gateway,
|
||||
sessionKey: sessionKey,
|
||||
voiceSessionId: voiceSessionId)
|
||||
@@ -4039,6 +4099,8 @@ extension TalkModeManager {
|
||||
.localized("Realtime disconnected")
|
||||
case "OpenClaw unavailable":
|
||||
.localized("OpenClaw unavailable")
|
||||
case "Confirmation needed":
|
||||
.localized("Confirmation needed")
|
||||
default:
|
||||
.verbatim(status)
|
||||
}
|
||||
@@ -4082,6 +4144,8 @@ extension TalkModeManager {
|
||||
String(localized: "Realtime disconnected")
|
||||
case "OpenClaw unavailable":
|
||||
String(localized: "OpenClaw unavailable")
|
||||
case "Confirmation needed":
|
||||
String(localized: "Confirmation needed")
|
||||
default:
|
||||
status
|
||||
}
|
||||
@@ -4227,7 +4291,7 @@ extension TalkModeManager {
|
||||
}
|
||||
guard shouldApply() else { return }
|
||||
self.pcmFormatUnavailable = false
|
||||
self.prefetchedRealtimeSession = nil
|
||||
self.invalidatePrefetchedRealtimeSession()
|
||||
let parsed = TalkModeGatewayConfigParser.parse(
|
||||
config: loaded.config,
|
||||
defaultProvider: Self.defaultTalkProvider,
|
||||
@@ -4739,6 +4803,18 @@ extension TalkModeManager: TalkRealtimeWebRTCSessionDelegate {
|
||||
self.lastSpokenText = trimmed
|
||||
}
|
||||
|
||||
func realtimeSession(
|
||||
_ session: TalkRealtimeWebRTCSession,
|
||||
didFailTranscriptPersistenceForEntry _: String,
|
||||
error _: Error)
|
||||
{
|
||||
guard session === self.realtimeSession else { return }
|
||||
self.setStatus(
|
||||
String(localized: "Chat error"),
|
||||
phase: self.phase,
|
||||
watchPresentation: .localized("Chat error"))
|
||||
}
|
||||
|
||||
func realtimeSessionDidFinish(_ session: TalkRealtimeWebRTCSession) {
|
||||
guard session === self.realtimeSession else { return }
|
||||
self.realtimeSession = nil
|
||||
@@ -4748,6 +4824,75 @@ extension TalkModeManager: TalkRealtimeWebRTCSessionDelegate {
|
||||
|
||||
#if DEBUG
|
||||
extension TalkModeManager {
|
||||
func _test_preparePrefetchedRealtimeVoiceSession(_ voiceSessionId: String) {
|
||||
self.activeRealtimeVoiceSessionId = voiceSessionId
|
||||
self.prefetchedRealtimeSession = TalkRealtimeClientSession(
|
||||
provider: "openai",
|
||||
transport: "webrtc",
|
||||
voiceSessionId: voiceSessionId,
|
||||
clientSecret: "test-client-secret",
|
||||
offerUrl: nil,
|
||||
offerHeaders: nil,
|
||||
model: "gpt-realtime-2",
|
||||
voice: "marin",
|
||||
expiresAt: nil)
|
||||
}
|
||||
|
||||
func _test_prepareLiveRealtimeVoiceSession(
|
||||
gateway: GatewayNodeSession,
|
||||
voiceSessionId: String,
|
||||
prefetchedVoiceSessionId: String)
|
||||
{
|
||||
self.activeRealtimeVoiceSessionId = voiceSessionId
|
||||
self.prefetchedRealtimeSession = TalkRealtimeClientSession(
|
||||
provider: "openai",
|
||||
transport: "webrtc",
|
||||
voiceSessionId: prefetchedVoiceSessionId,
|
||||
clientSecret: "test-prefetch-secret",
|
||||
offerUrl: nil,
|
||||
offerHeaders: nil,
|
||||
model: "gpt-realtime-2",
|
||||
voice: "marin",
|
||||
expiresAt: nil)
|
||||
self.realtimeSession = TalkRealtimeWebRTCSession(
|
||||
gateway: gateway,
|
||||
sessionKey: self.mainSessionKey,
|
||||
voiceSessionId: voiceSessionId,
|
||||
transcriptStore: self.realtimeTranscriptStore,
|
||||
delegate: self)
|
||||
}
|
||||
|
||||
func _test_invalidatePrefetchedRealtimeSession() async {
|
||||
await self.invalidatePrefetchedRealtimeSession()?.value
|
||||
}
|
||||
|
||||
func _test_activeRealtimeVoiceSessionId() -> String? {
|
||||
self.activeRealtimeVoiceSessionId
|
||||
}
|
||||
|
||||
func _test_hasPrefetchedRealtimeSession() -> Bool {
|
||||
self.prefetchedRealtimeSession != nil
|
||||
}
|
||||
|
||||
func _test_clearRealtimeSession() {
|
||||
self.realtimeSession = nil
|
||||
}
|
||||
|
||||
func _test_setRealtimeVoiceSessionCloseRequest(
|
||||
_ handler: (@MainActor (_ method: String, _ paramsJSON: String?) async throws -> Void)?)
|
||||
{
|
||||
self.testRealtimeVoiceSessionCloseRequest = handler
|
||||
}
|
||||
|
||||
static func _test_retryRealtimeVoiceSessionClose(
|
||||
operation: @escaping @MainActor () async throws -> Void) async throws
|
||||
{
|
||||
try await self.retryRealtimeVoiceSessionClose(
|
||||
retryDelaysNanoseconds: [0, 1, 1],
|
||||
sleep: { _ in },
|
||||
operation: operation)
|
||||
}
|
||||
|
||||
static func _test_shouldRestartRealtimeSession(
|
||||
isEnabled: Bool,
|
||||
gatewayConnected: Bool,
|
||||
|
||||
@@ -15,6 +15,10 @@ protocol TalkRealtimeWebRTCSessionDelegate: AnyObject {
|
||||
func realtimeSession(_ session: TalkRealtimeWebRTCSession, didUpdateAudioLevels input: Double?, output: Double?)
|
||||
func realtimeSession(_ session: TalkRealtimeWebRTCSession, didReceiveUserTranscript text: String)
|
||||
func realtimeSession(_ session: TalkRealtimeWebRTCSession, didReceiveAssistantTranscript text: String)
|
||||
func realtimeSession(
|
||||
_ session: TalkRealtimeWebRTCSession,
|
||||
didFailTranscriptPersistenceForEntry entryId: String,
|
||||
error: Error)
|
||||
func realtimeSessionDidFinish(_ session: TalkRealtimeWebRTCSession)
|
||||
}
|
||||
|
||||
@@ -440,12 +444,26 @@ final class TalkRealtimeWebRTCSession: NSObject {
|
||||
paramsJSON: json,
|
||||
timeoutSeconds: 5)
|
||||
},
|
||||
failureLog: { entryId, error in
|
||||
GatewayDiagnostics.log(
|
||||
"talk transcript persist FAILED entryId=\(entryId) error=\(error.localizedDescription)")
|
||||
failureLog: { [weak self] entryId, error in
|
||||
self?.reportTranscriptPersistenceFailure(entryId: entryId, error: error)
|
||||
})
|
||||
}
|
||||
|
||||
private func reportTranscriptPersistenceFailure(entryId: String, error: Error) {
|
||||
GatewayDiagnostics.log(
|
||||
"talk transcript persist FAILED entryId=\(entryId) error=\(error.localizedDescription)")
|
||||
self.delegate?.realtimeSession(
|
||||
self,
|
||||
didFailTranscriptPersistenceForEntry: entryId,
|
||||
error: error)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
func _test_reportTranscriptPersistenceFailure(entryId: String, error: Error) {
|
||||
self.reportTranscriptPersistenceFailure(entryId: entryId, error: error)
|
||||
}
|
||||
#endif
|
||||
|
||||
func flushTranscriptWrites() async {
|
||||
guard let voiceSessionId = self.voiceSessionId else { return }
|
||||
await self.transcriptStore.flush(voiceSessionId: voiceSessionId)
|
||||
|
||||
@@ -6,6 +6,8 @@ import Testing
|
||||
|
||||
@MainActor
|
||||
struct TalkModeManagerTests {
|
||||
private struct CloseError: Error {}
|
||||
|
||||
@Test func `encodes realtime client voice session identity`() throws {
|
||||
let params = TalkRealtimeClientCreateParams(
|
||||
sessionKey: "agent:main:main",
|
||||
@@ -36,6 +38,63 @@ struct TalkModeManagerTests {
|
||||
#expect(serverOwned.voiceSessionId == nil)
|
||||
}
|
||||
|
||||
@Test func `config invalidation closes and clears an adopted realtime prefetch`() async throws {
|
||||
let manager = TalkModeManager(allowSimulatorCapture: true)
|
||||
let gateway = GatewayNodeSession()
|
||||
manager.attachGateway(gateway)
|
||||
var closeRequests: [(method: String, paramsJSON: String?)] = []
|
||||
manager._test_setRealtimeVoiceSessionCloseRequest { method, paramsJSON in
|
||||
closeRequests.append((method, paramsJSON))
|
||||
}
|
||||
manager._test_preparePrefetchedRealtimeVoiceSession("vs-A")
|
||||
|
||||
await manager._test_invalidatePrefetchedRealtimeSession()
|
||||
|
||||
#expect(manager._test_activeRealtimeVoiceSessionId() == nil)
|
||||
#expect(!manager._test_hasPrefetchedRealtimeSession())
|
||||
let request = try #require(closeRequests.first)
|
||||
#expect(closeRequests.count == 1)
|
||||
#expect(request.method == "talk.client.close")
|
||||
let json = try #require(request.paramsJSON?.data(using: .utf8))
|
||||
let params = try #require(JSONSerialization.jsonObject(with: json) as? [String: String])
|
||||
#expect(params["voiceSessionId"] == "vs-A")
|
||||
#expect(params["sessionKey"] == "main")
|
||||
}
|
||||
|
||||
@Test func `config invalidation preserves a live realtime voice session`() async {
|
||||
let manager = TalkModeManager(allowSimulatorCapture: true)
|
||||
let gateway = GatewayNodeSession()
|
||||
manager.attachGateway(gateway)
|
||||
var closeRequestCount = 0
|
||||
manager._test_setRealtimeVoiceSessionCloseRequest { _, _ in
|
||||
closeRequestCount += 1
|
||||
}
|
||||
manager._test_prepareLiveRealtimeVoiceSession(
|
||||
gateway: gateway,
|
||||
voiceSessionId: "vs-live",
|
||||
prefetchedVoiceSessionId: "vs-unused")
|
||||
|
||||
await manager._test_invalidatePrefetchedRealtimeSession()
|
||||
|
||||
#expect(manager._test_activeRealtimeVoiceSessionId() == "vs-live")
|
||||
#expect(!manager._test_hasPrefetchedRealtimeSession())
|
||||
#expect(closeRequestCount == 0)
|
||||
manager._test_clearRealtimeSession()
|
||||
}
|
||||
|
||||
@Test func `retries realtime voice session close three times`() async throws {
|
||||
var attempts = 0
|
||||
|
||||
try await TalkModeManager._test_retryRealtimeVoiceSessionClose {
|
||||
attempts += 1
|
||||
if attempts < 3 {
|
||||
throw CloseError()
|
||||
}
|
||||
}
|
||||
|
||||
#expect(attempts == 3)
|
||||
}
|
||||
|
||||
@Test func `encodes transcript entry id as a decimal string`() throws {
|
||||
let params = TalkRealtimeTranscriptParams(
|
||||
sessionKey: "agent:main:main",
|
||||
@@ -459,6 +518,10 @@ struct TalkModeManagerTests {
|
||||
manager._test_handleRealtimeRelayStatus("Backend rejected realtime request")
|
||||
#expect(manager.watchPresentation == .verbatim("Backend rejected realtime request"))
|
||||
|
||||
manager._test_handleRealtimeRelayStatus("Confirmation needed")
|
||||
#expect(manager.statusText == String(localized: "Confirmation needed"))
|
||||
#expect(manager.watchPresentation == .localized("Confirmation needed"))
|
||||
|
||||
manager._test_handleRealtimeRelayStatus("Reconnecting")
|
||||
#expect(manager.phase == .connecting)
|
||||
#expect(manager.watchPresentation == .phase)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import OpenClawKit
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
@@ -8,8 +9,14 @@ struct TalkRealtimeTranscriptWriteQueueTests {
|
||||
|
||||
@Test func `retries writes in order and continues after final failure`() async {
|
||||
var attempts: [String] = []
|
||||
var failures: [String] = []
|
||||
let store = TalkRealtimeTranscriptStore(retryDelaysNanoseconds: [0, 0, 0, 0])
|
||||
let delegate = RecordingTranscriptFailureDelegate()
|
||||
let session = TalkRealtimeWebRTCSession(
|
||||
gateway: GatewayNodeSession(),
|
||||
sessionKey: "agent:main:main",
|
||||
voiceSessionId: "voice-1",
|
||||
transcriptStore: store,
|
||||
delegate: delegate)
|
||||
|
||||
for _ in 1...3 {
|
||||
store.enqueue(
|
||||
@@ -27,14 +34,14 @@ struct TalkRealtimeTranscriptWriteQueueTests {
|
||||
throw PersistError()
|
||||
}
|
||||
},
|
||||
failureLog: { entryId, _ in
|
||||
failures.append(entryId)
|
||||
failureLog: { entryId, error in
|
||||
session._test_reportTranscriptPersistenceFailure(entryId: entryId, error: error)
|
||||
})
|
||||
}
|
||||
await store.flush(voiceSessionId: "voice-1")
|
||||
|
||||
#expect(attempts == ["1", "1", "1", "2", "2", "2", "3"])
|
||||
#expect(failures == ["2"])
|
||||
#expect(delegate.failedEntryIds == ["2"])
|
||||
}
|
||||
|
||||
@Test func `shares ordering across transport replacements`() async {
|
||||
@@ -79,3 +86,24 @@ struct TalkRealtimeTranscriptWriteQueueTests {
|
||||
#expect(persisted.filter { $0.hasPrefix("voice-b:") } == ["voice-b:1"])
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private final class RecordingTranscriptFailureDelegate: TalkRealtimeWebRTCSessionDelegate {
|
||||
var failedEntryIds: [String] = []
|
||||
|
||||
func realtimeSession(_: TalkRealtimeWebRTCSession, didChangeStatus _: String) {}
|
||||
func realtimeSession(_: TalkRealtimeWebRTCSession, didDetectInputSpeech _: Bool) {}
|
||||
func realtimeSession(_: TalkRealtimeWebRTCSession, didUpdateAudioLevels _: Double?, output _: Double?) {}
|
||||
func realtimeSession(_: TalkRealtimeWebRTCSession, didReceiveUserTranscript _: String) {}
|
||||
func realtimeSession(_: TalkRealtimeWebRTCSession, didReceiveAssistantTranscript _: String) {}
|
||||
|
||||
func realtimeSession(
|
||||
_: TalkRealtimeWebRTCSession,
|
||||
didFailTranscriptPersistenceForEntry entryId: String,
|
||||
error _: Error)
|
||||
{
|
||||
self.failedEntryIds.append(entryId)
|
||||
}
|
||||
|
||||
func realtimeSessionDidFinish(_: TalkRealtimeWebRTCSession) {}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user