diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json index 0291a80ae28..8ba233197bf 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-chunk-speech.dictation.chunk-1.json @@ -4,11 +4,7 @@ "family": "speech.dictation-chunk", "namedDeltas": [], "values": { - "02cebe6f0062": { - "failures": [], - "pending": 0 - }, - "0a3ec57e0321": { + "028afac3649e": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -27,7 +23,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -47,84 +43,15 @@ } } }, + "02cebe6f0062": { + "failures": [], + "pending": 0 + }, "12521d219054": { "name": "speech.dictation.chunk#1", "ordinal": 2, "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.chunk\",\"params\":{\"dictationId\":\"dictation-1\",\"audioBase64\":\"ACVKb5S53gM=\",\"sampleRate\":16000}}" }, - "16da7db6669b": { - "name": "speech.dictation.chunk#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.chunk" - }, - { - "name": "params", - "value": { - "audioBase64": "ACVKb5S53gM=", - "dictationId": "dictation-1", - "sampleRate": 16000 - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "error": "inner refused", - "ok": false - } - } - } - }, - "182109450de8": { - "name": "speech.dictation.chunk#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.chunk" - }, - { - "name": "params", - "value": { - "audioBase64": "ACVKb5S53gM=", - "dictationId": "dictation-1", - "sampleRate": 16000 - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "received": true - } - } - } - }, "19c961e90970": { "name": "dictation-failed", "ordinal": 3, @@ -132,7 +59,11 @@ "id": "dictation-1" } }, - "1ca8c123cb2d": { + "2762d465637e": { + "failures": ["Unknown method"], + "pending": 0 + }, + "30ae620f58dc": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -151,7 +82,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -166,7 +97,7 @@ } } }, - "1d0f84aa0730": { + "62cfcef21f01": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -185,7 +116,40 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "66dece39b5ba": { + "name": "speech.dictation.chunk#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.chunk" + }, + { + "name": "params", + "value": { + "audioBase64": "ACVKb5S53gM=", + "dictationId": "dictation-1", + "sampleRate": 16000 + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 } } ], @@ -203,7 +167,7 @@ } } }, - "208244a4b935": { + "83a5bdf2ac1a": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -222,44 +186,7 @@ { "name": "options", "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "method_not_found", - "message": "Unknown method" - }, - "id": "frame-1", - "ok": false - } - } - }, - "25256657a918": { - "name": "speech.dictation.chunk#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.chunk" - }, - { - "name": "params", - "value": { - "audioBase64": "ACVKb5S53gM=", - "dictationId": "dictation-1", - "sampleRate": 16000 - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -269,19 +196,14 @@ "settledAt": 0, "value": { "id": "frame-1", - "ok": true + "ok": true, + "result": { + "error": "refused" + } } } }, - "2762d465637e": { - "failures": ["Unknown method"], - "pending": 0 - }, - "28366801162a": { - "failures": ["transport failure"], - "pending": 0 - }, - "88a268fa1b7b": { + "888a29b4fd97": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -300,7 +222,80 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "99956eef8fff": { + "name": "speech.dictation.chunk#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.chunk" + }, + { + "name": "params", + "value": { + "audioBase64": "ACVKb5S53gM=", + "dictationId": "dictation-1", + "sampleRate": 16000 + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "received": true + } + } + } + }, + "9d43743b1b10": { + "name": "speech.dictation.chunk#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.chunk" + }, + { + "name": "params", + "value": { + "audioBase64": "ACVKb5S53gM=", + "dictationId": "dictation-1", + "sampleRate": 16000 + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 } } ], @@ -315,83 +310,10 @@ } } }, - "923922ffae75": { - "name": "speech.dictation.chunk#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.chunk" - }, - { - "name": "params", - "value": { - "audioBase64": "ACVKb5S53gM=", - "dictationId": "dictation-1", - "sampleRate": 16000 - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "$rpc": "null" - } - } - } - }, "a2f842e44f38": { "failures": ["outer refused"], "pending": 0 }, - "a5db8d0cb481": { - "name": "speech.dictation.chunk#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.chunk" - }, - { - "name": "params", - "value": { - "audioBase64": "ACVKb5S53gM=", - "dictationId": "dictation-1", - "sampleRate": 16000 - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "refused", - "message": "" - }, - "id": "frame-1", - "ok": false - } - } - }, "b5157118341f": { "failures": [""], "pending": 0 @@ -409,7 +331,7 @@ } ] }, - "e9501f2e1278": { + "da1579f3254f": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -428,7 +350,81 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "f6a8a6f550f5": { + "name": "speech.dictation.chunk#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.chunk" + }, + { + "name": "params", + "value": { + "audioBase64": "ACVKb5S53gM=", + "dictationId": "dictation-1", + "sampleRate": 16000 + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "f779babe2a18": { + "name": "speech.dictation.chunk#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.chunk" + }, + { + "name": "params", + "value": { + "audioBase64": "ACVKb5S53gM=", + "dictationId": "dictation-1", + "sampleRate": 16000 + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 } } ], @@ -440,7 +436,7 @@ "id": "frame-1", "ok": true, "result": { - "error": "refused" + "$rpc": "null" } } } @@ -452,7 +448,7 @@ { "id": "speech-audio-chunk-acknowledged.normal:acknowledged", "observation": { - "sender": ["182109450de8"], + "sender": ["99956eef8fff"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -464,7 +460,7 @@ { "id": "speech-audio-chunk-acknowledged.result-absent:acknowledged", "observation": { - "sender": ["25256657a918"], + "sender": ["62cfcef21f01"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -476,7 +472,7 @@ { "id": "speech-audio-chunk-acknowledged.result-null:acknowledged", "observation": { - "sender": ["923922ffae75"], + "sender": ["f779babe2a18"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -488,7 +484,7 @@ { "id": "speech-audio-chunk-acknowledged.inner-ok-missing:acknowledged", "observation": { - "sender": ["e9501f2e1278"], + "sender": ["83a5bdf2ac1a"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -500,7 +496,7 @@ { "id": "speech-audio-chunk-acknowledged.inner-false-string-error:acknowledged", "observation": { - "sender": ["16da7db6669b"], + "sender": ["888a29b4fd97"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -512,7 +508,7 @@ { "id": "speech-audio-chunk-acknowledged.inner-false-object-error:acknowledged", "observation": { - "sender": ["0a3ec57e0321"], + "sender": ["028afac3649e"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -524,7 +520,7 @@ { "id": "speech-audio-chunk-acknowledged.outer-refused:acknowledged", "observation": { - "sender": ["1d0f84aa0730"], + "sender": ["66dece39b5ba"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -536,7 +532,7 @@ { "id": "speech-audio-chunk-acknowledged.outer-refused-no-message:acknowledged", "observation": { - "sender": ["a5db8d0cb481"], + "sender": ["f6a8a6f550f5"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -548,7 +544,7 @@ { "id": "speech-audio-chunk-acknowledged.method-not-found:acknowledged", "observation": { - "sender": ["208244a4b935"], + "sender": ["da1579f3254f"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" @@ -560,25 +556,25 @@ { "id": "speech-audio-chunk-acknowledged.transport-rejection:acknowledged", "observation": { - "sender": ["1ca8c123cb2d"], + "sender": ["30ae620f58dc"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" }, - "state": "28366801162a", - "effects": ["19c961e90970"] + "state": "02cebe6f0062", + "effects": [] } }, { "id": "speech-audio-chunk-acknowledged.transport-rejection-no-message:acknowledged", "observation": { - "sender": ["88a268fa1b7b"], + "sender": ["9d43743b1b10"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" }, - "state": "b5157118341f", - "effects": ["19c961e90970"] + "state": "02cebe6f0062", + "effects": [] } } ] diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json index 7c0ace69617..2a94882464f 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.finish-1.json @@ -262,6 +262,32 @@ } } }, + "893e5291c492": { + "name": "speech.dictation.cancel#1", + "ordinal": 5, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, "97af9f7912fc": { "name": "dictation-error", "ordinal": 6, @@ -351,66 +377,6 @@ } } }, - "a7dbb6b2864f": { - "name": "speech.dictation.cancel#1", - "ordinal": 5, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "pending", - "startedAt": 0 - } - }, - "aca401ac072e": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "started": true - } - } - } - }, "b41f74be4c3d": { "name": "speech.dictation.finish#1", "ordinal": 3, @@ -458,6 +424,40 @@ "message": "Unknown method" } }, + "bd004568e24c": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "started": true + } + } + } + }, "cd49b339a081": { "name": "dictation-error", "ordinal": 6, @@ -559,7 +559,7 @@ { "id": "speech-dictation-session-transcript.normal:transcribed", "observation": { - "sender": ["aca401ac072e", "4728ad10f92b"], + "sender": ["bd004568e24c", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -573,7 +573,7 @@ { "id": "speech-dictation-session-transcript.result-absent:transcribed", "observation": { - "sender": ["aca401ac072e", "88f795a14a21", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "88f795a14a21", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", @@ -587,7 +587,7 @@ { "id": "speech-dictation-session-transcript.result-null:transcribed", "observation": { - "sender": ["aca401ac072e", "349e2cfd1a1f", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "349e2cfd1a1f", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", @@ -601,7 +601,7 @@ { "id": "speech-dictation-session-transcript.inner-ok-missing:transcribed", "observation": { - "sender": ["aca401ac072e", "07d3e2ed39d8"], + "sender": ["bd004568e24c", "07d3e2ed39d8"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -615,7 +615,7 @@ { "id": "speech-dictation-session-transcript.inner-false-string-error:transcribed", "observation": { - "sender": ["aca401ac072e", "f4cf5aad78c0"], + "sender": ["bd004568e24c", "f4cf5aad78c0"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -629,7 +629,7 @@ { "id": "speech-dictation-session-transcript.inner-false-object-error:transcribed", "observation": { - "sender": ["aca401ac072e", "74617082bf69"], + "sender": ["bd004568e24c", "74617082bf69"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -643,7 +643,7 @@ { "id": "speech-dictation-session-transcript.outer-refused:transcribed", "observation": { - "sender": ["aca401ac072e", "a46c0f6c95b2", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "a46c0f6c95b2", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", @@ -657,7 +657,7 @@ { "id": "speech-dictation-session-transcript.outer-refused-no-message:transcribed", "observation": { - "sender": ["aca401ac072e", "9fce1a80008e", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "9fce1a80008e", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", @@ -671,7 +671,7 @@ { "id": "speech-dictation-session-transcript.method-not-found:transcribed", "observation": { - "sender": ["aca401ac072e", "b41f74be4c3d", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "b41f74be4c3d", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", @@ -685,7 +685,7 @@ { "id": "speech-dictation-session-transcript.transport-rejection:transcribed", "observation": { - "sender": ["aca401ac072e", "cf10282da8e0", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "cf10282da8e0", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", @@ -699,7 +699,7 @@ { "id": "speech-dictation-session-transcript.transport-rejection-no-message:transcribed", "observation": { - "sender": ["aca401ac072e", "6d6bb34ce2ab", "a7dbb6b2864f"], + "sender": ["bd004568e24c", "6d6bb34ce2ab", "893e5291c492"], "payloads": ["859d310e63d6", "1bdc94d60271", "6c3d0754a1ff"], "settlements": { "mount": "eb79a9b3682a", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json index 0455728f889..b259a3754c5 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-session-speech.dictation.start-1.json @@ -4,12 +4,7 @@ "family": "speech.dictation-session", "namedDeltas": [], "values": { - "1bdc94d60271": { - "name": "speech.dictation.finish#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.finish\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" - }, - "2104f46e44bd": { + "031fac8ed0c3": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -26,28 +21,31 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], "settlement": { - "status": "rejected", + "status": "fulfilled", "startedAt": 0, "settledAt": 0, - "error": { - "category": "Error", - "message": "", - "isRpcDeliveryUnknown": true + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } } } }, - "355b3eaef1fb": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, + "062d9c83659b": { + "name": "speech.dictation.start#1", + "ordinal": 1, "args": [ { "name": "method", - "value": "speech.dictation.cancel" + "value": "speech.dictation.start" }, { "name": "params", @@ -58,15 +56,92 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], "settlement": { - "status": "pending", - "startedAt": 0 + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } } }, + "104a53ec5972": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "136c60a2a62f": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } + }, + "1bdc94d60271": { + "name": "speech.dictation.finish#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.finish\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" + }, "4728ad10f92b": { "name": "speech.dictation.finish#1", "ordinal": 3, @@ -101,7 +176,7 @@ } } }, - "5655ac21ce64": { + "6d8188e86d2a": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -118,7 +193,219 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "" + }, + "id": "frame-1", + "ok": false + } + } + }, + "859d310e63d6": { + "name": "speech.dictation.start#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" + }, + "85d47816bfc5": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "pending", + "startedAt": 0 + } + }, + "9270aeb7d9c6": { + "status": "pending", + "startedAt": 0 + }, + "a19279fc9c65": { + "error": { + "$rpc": "null" + }, + "status": "idle", + "transcripts": ["hello world"] + }, + "a8dc23386147": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "b7421e659b44": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "bd004568e24c": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "started": true + } + } + } + }, + "c9d7f3d355eb": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "e05190caf800": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 } } ], @@ -138,224 +425,6 @@ } } }, - "60ac03a80551": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "refused", - "message": "outer refused" - }, - "id": "frame-1", - "ok": false - } - } - }, - "624d174d4477": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "transport failure", - "isRpcDeliveryUnknown": true - } - } - }, - "68f011596870": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true - } - } - }, - "859d310e63d6": { - "name": "speech.dictation.start#1", - "ordinal": 2, - "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" - }, - "9270aeb7d9c6": { - "status": "pending", - "startedAt": 0 - }, - "a19279fc9c65": { - "error": { - "$rpc": "null" - }, - "status": "idle", - "transcripts": ["hello world"] - }, - "aca401ac072e": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "started": true - } - } - } - }, - "b05828b4ea8e": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "refused", - "message": "" - }, - "id": "frame-1", - "ok": false - } - } - }, - "baf7ef332fbf": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "error": "inner refused", - "ok": false - } - } - } - }, "eb79a9b3682a": { "status": "fulfilled", "startedAt": 0, @@ -369,7 +438,14 @@ "ordinal": 4, "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.cancel\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" }, - "eebe8f57e84a": { + "f6a74c428142": { + "error": { + "$rpc": "null" + }, + "status": "starting", + "transcripts": [] + }, + "fb132fa96492": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -386,7 +462,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -402,82 +478,6 @@ } } } - }, - "f6a74c428142": { - "error": { - "$rpc": "null" - }, - "status": "starting", - "transcripts": [] - }, - "fa7299197bf0": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "error": "refused" - } - } - } - }, - "fcb906b12a66": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "method_not_found", - "message": "Unknown method" - }, - "id": "frame-1", - "ok": false - } - } } }, "recording": { @@ -486,7 +486,7 @@ { "id": "speech-dictation-session-transcript.normal:transcribed", "observation": { - "sender": ["aca401ac072e", "4728ad10f92b"], + "sender": ["bd004568e24c", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -500,7 +500,7 @@ { "id": "speech-dictation-session-transcript.result-absent:transcribed", "observation": { - "sender": ["68f011596870", "4728ad10f92b"], + "sender": ["136c60a2a62f", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -514,7 +514,7 @@ { "id": "speech-dictation-session-transcript.result-null:transcribed", "observation": { - "sender": ["eebe8f57e84a", "4728ad10f92b"], + "sender": ["fb132fa96492", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -528,7 +528,7 @@ { "id": "speech-dictation-session-transcript.inner-ok-missing:transcribed", "observation": { - "sender": ["fa7299197bf0", "4728ad10f92b"], + "sender": ["a8dc23386147", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -542,7 +542,7 @@ { "id": "speech-dictation-session-transcript.inner-false-string-error:transcribed", "observation": { - "sender": ["baf7ef332fbf", "4728ad10f92b"], + "sender": ["031fac8ed0c3", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -556,7 +556,7 @@ { "id": "speech-dictation-session-transcript.inner-false-object-error:transcribed", "observation": { - "sender": ["5655ac21ce64", "4728ad10f92b"], + "sender": ["e05190caf800", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", @@ -570,7 +570,7 @@ { "id": "speech-dictation-session-transcript.outer-refused:transcribed", "observation": { - "sender": ["60ac03a80551", "355b3eaef1fb"], + "sender": ["b7421e659b44", "85d47816bfc5"], "payloads": ["859d310e63d6", "ebdac948a90d"], "settlements": { "mount": "eb79a9b3682a", @@ -584,7 +584,7 @@ { "id": "speech-dictation-session-transcript.outer-refused-no-message:transcribed", "observation": { - "sender": ["b05828b4ea8e", "355b3eaef1fb"], + "sender": ["6d8188e86d2a", "85d47816bfc5"], "payloads": ["859d310e63d6", "ebdac948a90d"], "settlements": { "mount": "eb79a9b3682a", @@ -598,7 +598,7 @@ { "id": "speech-dictation-session-transcript.method-not-found:transcribed", "observation": { - "sender": ["fcb906b12a66", "355b3eaef1fb"], + "sender": ["062d9c83659b", "85d47816bfc5"], "payloads": ["859d310e63d6", "ebdac948a90d"], "settlements": { "mount": "eb79a9b3682a", @@ -612,7 +612,7 @@ { "id": "speech-dictation-session-transcript.transport-rejection:transcribed", "observation": { - "sender": ["624d174d4477", "355b3eaef1fb"], + "sender": ["104a53ec5972", "85d47816bfc5"], "payloads": ["859d310e63d6", "ebdac948a90d"], "settlements": { "mount": "eb79a9b3682a", @@ -626,7 +626,7 @@ { "id": "speech-dictation-session-transcript.transport-rejection-no-message:transcribed", "observation": { - "sender": ["2104f46e44bd", "355b3eaef1fb"], + "sender": ["c9d7f3d355eb", "85d47816bfc5"], "payloads": ["859d310e63d6", "ebdac948a90d"], "settlements": { "mount": "eb79a9b3682a", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json index ef37367d2b4..3b20c5a8e33 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.cancel-1.json @@ -4,7 +4,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "values": { - "02d3c28ecfb6": { + "0b8b17cc7b58": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -21,176 +21,7 @@ { "name": "options", "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "$rpc": "null" - } - } - } - }, - "1710d94f25d0": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "error": "inner refused", - "ok": false - } - } - } - }, - "25f8db2d01b6": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "", - "isRpcDeliveryUnknown": true - } - } - }, - "3b5708754539": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "started": true - } - } - } - }, - "46564194b0d9": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "error": "refused" - } - } - } - }, - "5f51c1b28c86": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" + "failWhenDisconnected": true } } ], @@ -208,7 +39,7 @@ } } }, - "73faa32df517": { + "2e3b79ca68ce": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -225,7 +56,39 @@ { "name": "options", "value": { - "$rpc": "absent" + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "32caef6aa162": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true } } ], @@ -235,20 +98,45 @@ "settledAt": 0, "value": { "id": "frame-2", + "ok": true + } + } + }, + "4c42677aca21": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", "ok": true, "result": { - "cancelled": true + "started": true } } } }, - "7ed3d39f0607": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": false - }, - "927c61fd29b5": { + "5699810cc2a2": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -265,47 +153,7 @@ { "name": "options", "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "method_not_found", - "message": "Unknown method" - }, - "id": "frame-2", - "ok": false - } - } - }, - "a7ddd021010a": { - "name": "speech.dictation.start#1", - "ordinal": 2, - "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"dictation-1\"}}" - }, - "cc661ef3e67e": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" + "failWhenDisconnected": true } } ], @@ -325,12 +173,7 @@ } } }, - "ce9cbc8837fa": { - "name": "speech.dictation.cancel#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.cancel\",\"params\":{\"dictationId\":\"dictation-1\"}}" - }, - "dbc1fdbc29d3": { + "6f5a48ebce0e": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -347,7 +190,121 @@ { "name": "options", "value": { - "$rpc": "absent" + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "7ed3d39f0607": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": false + }, + "9ab91ee194b8": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "cancelled": true + } + } + } + }, + "a7ddd021010a": { + "name": "speech.dictation.start#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"dictation-1\"}}" + }, + "b20560e60f90": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-2", + "ok": false + } + } + }, + "b90df4b1e290": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true } } ], @@ -365,14 +322,12 @@ } } }, - "e0fcd8f8c1a9": { - "activeId": { - "$rpc": "null" - }, - "idle": false, - "started": false + "ce9cbc8837fa": { + "name": "speech.dictation.cancel#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.cancel\",\"params\":{\"dictationId\":\"dictation-1\"}}" }, - "e59c2b1cff33": { + "d0501bb1dd67": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -389,7 +344,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "failWhenDisconnected": true } } ], @@ -399,19 +354,15 @@ "settledAt": 0, "value": { "id": "frame-2", - "ok": true + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } } } }, - "eb79a9b3682a": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "$rpc": "undefined" - } - }, - "f3ac8ed5616e": { + "d80561ce52f2": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -428,7 +379,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "failWhenDisconnected": true } } ], @@ -442,6 +393,55 @@ "isRpcDeliveryUnknown": true } } + }, + "df38de088787": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "e0fcd8f8c1a9": { + "activeId": { + "$rpc": "null" + }, + "idle": false, + "started": false + }, + "eb79a9b3682a": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "$rpc": "undefined" + } } }, "recording": { @@ -450,7 +450,7 @@ { "id": "speech-desktop-start-superseded.normal:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "73faa32df517"], + "sender": ["4c42677aca21", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -463,7 +463,7 @@ { "id": "speech-desktop-start-superseded.result-absent:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "e59c2b1cff33"], + "sender": ["4c42677aca21", "32caef6aa162"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -476,7 +476,7 @@ { "id": "speech-desktop-start-superseded.result-null:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "02d3c28ecfb6"], + "sender": ["4c42677aca21", "6f5a48ebce0e"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -489,7 +489,7 @@ { "id": "speech-desktop-start-superseded.inner-ok-missing:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "46564194b0d9"], + "sender": ["4c42677aca21", "df38de088787"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -502,7 +502,7 @@ { "id": "speech-desktop-start-superseded.inner-false-string-error:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "1710d94f25d0"], + "sender": ["4c42677aca21", "d0501bb1dd67"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -515,7 +515,7 @@ { "id": "speech-desktop-start-superseded.inner-false-object-error:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "cc661ef3e67e"], + "sender": ["4c42677aca21", "5699810cc2a2"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -528,7 +528,7 @@ { "id": "speech-desktop-start-superseded.outer-refused:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "5f51c1b28c86"], + "sender": ["4c42677aca21", "0b8b17cc7b58"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -541,7 +541,7 @@ { "id": "speech-desktop-start-superseded.outer-refused-no-message:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "dbc1fdbc29d3"], + "sender": ["4c42677aca21", "b90df4b1e290"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -554,7 +554,7 @@ { "id": "speech-desktop-start-superseded.method-not-found:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "927c61fd29b5"], + "sender": ["4c42677aca21", "b20560e60f90"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -567,7 +567,7 @@ { "id": "speech-desktop-start-superseded.transport-rejection:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "f3ac8ed5616e"], + "sender": ["4c42677aca21", "d80561ce52f2"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -580,7 +580,7 @@ { "id": "speech-desktop-start-superseded.transport-rejection-no-message:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "25f8db2d01b6"], + "sender": ["4c42677aca21", "2e3b79ca68ce"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", diff --git a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json index 1f50752b0df..ab98d6156c9 100644 --- a/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json +++ b/mobile/rpc-foundation/goldens/matrix-speech.dictation-start-speech.dictation.start-1.json @@ -4,7 +4,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "values": { - "00ab3d9734bb": { + "02e3f6a398b9": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -21,354 +21,7 @@ { "name": "options", "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true - } - } - }, - "0ec7faed0e3d": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "transport failure", - "isRpcDeliveryUnknown": true - } - } - }, - "38b3ced9bf9c": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "error": "refused" - } - } - } - }, - "3b5708754539": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "started": true - } - } - } - }, - "3c4f020193c4": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "method_not_found", - "message": "Unknown method" - }, - "id": "frame-1", - "ok": false - } - } - }, - "50620ee89249": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "rejected", - "startedAt": 0, - "settledAt": 0, - "error": { - "category": "Error", - "message": "", - "isRpcDeliveryUnknown": true - } - } - }, - "55019d971687": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "error": "inner refused", - "ok": false - } - } - } - }, - "5bd3d1bd4863": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "error": { - "code": "refused", - "message": "outer refused" - }, - "id": "frame-1", - "ok": false - } - } - }, - "73faa32df517": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "cancelled": true - } - } - } - }, - "7ed3d39f0607": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": false - }, - "a72f6bbdd0b3": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-1", - "ok": true, - "result": { - "$rpc": "null" - } - } - } - }, - "a7ddd021010a": { - "name": "speech.dictation.start#1", - "ordinal": 2, - "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"dictation-1\"}}" - }, - "ba48be88195e": { - "name": "speech.dictation.start#1", - "ordinal": 1, - "args": [ - { - "name": "method", - "value": "speech.dictation.start" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -386,12 +39,7 @@ } } }, - "ce9cbc8837fa": { - "name": "speech.dictation.cancel#1", - "ordinal": 4, - "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.cancel\",\"params\":{\"dictationId\":\"dictation-1\"}}" - }, - "ceca67daa178": { + "0ac1d1294a4a": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -408,7 +56,224 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "$rpc": "null" + } + } + } + }, + "106b709a0579": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "", + "isRpcDeliveryUnknown": true + } + } + }, + "4c42677aca21": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "started": true + } + } + } + }, + "7ed3d39f0607": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": false + }, + "9ab91ee194b8": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "cancelled": true + } + } + } + }, + "9c6249e03654": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "refused", + "message": "outer refused" + }, + "id": "frame-1", + "ok": false + } + } + }, + "a1030fe3abd4": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "rejected", + "startedAt": 0, + "settledAt": 0, + "error": { + "category": "Error", + "message": "transport failure", + "isRpcDeliveryUnknown": true + } + } + }, + "a7ddd021010a": { + "name": "speech.dictation.start#1", + "ordinal": 2, + "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"dictation-1\"}}" + }, + "ce9cbc8837fa": { + "name": "speech.dictation.cancel#1", + "ordinal": 4, + "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.cancel\",\"params\":{\"dictationId\":\"dictation-1\"}}" + }, + "ced9bd128f52": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 } } ], @@ -442,6 +307,141 @@ "value": { "$rpc": "undefined" } + }, + "f010ef5b6bfc": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "refused" + } + } + } + }, + "f2166355786a": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "error": { + "code": "method_not_found", + "message": "Unknown method" + }, + "id": "frame-1", + "ok": false + } + } + }, + "f2eeb517059d": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true, + "result": { + "error": "inner refused", + "ok": false + } + } + } + }, + "fe8f7e77754e": { + "name": "speech.dictation.start#1", + "ordinal": 1, + "args": [ + { + "name": "method", + "value": "speech.dictation.start" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "timeoutMs": 30000 + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-1", + "ok": true + } + } } }, "recording": { @@ -450,7 +450,7 @@ { "id": "speech-desktop-start-superseded.normal:stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "73faa32df517"], + "sender": ["4c42677aca21", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -463,7 +463,7 @@ { "id": "speech-desktop-start-superseded.result-absent:stale-start-cancelled", "observation": { - "sender": ["00ab3d9734bb", "73faa32df517"], + "sender": ["fe8f7e77754e", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -476,7 +476,7 @@ { "id": "speech-desktop-start-superseded.result-null:stale-start-cancelled", "observation": { - "sender": ["a72f6bbdd0b3", "73faa32df517"], + "sender": ["0ac1d1294a4a", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -489,7 +489,7 @@ { "id": "speech-desktop-start-superseded.inner-ok-missing:stale-start-cancelled", "observation": { - "sender": ["38b3ced9bf9c", "73faa32df517"], + "sender": ["f010ef5b6bfc", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -502,7 +502,7 @@ { "id": "speech-desktop-start-superseded.inner-false-string-error:stale-start-cancelled", "observation": { - "sender": ["55019d971687", "73faa32df517"], + "sender": ["f2eeb517059d", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -515,7 +515,7 @@ { "id": "speech-desktop-start-superseded.inner-false-object-error:stale-start-cancelled", "observation": { - "sender": ["ceca67daa178", "73faa32df517"], + "sender": ["ced9bd128f52", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -528,7 +528,7 @@ { "id": "speech-desktop-start-superseded.outer-refused:stale-start-cancelled", "observation": { - "sender": ["5bd3d1bd4863", "73faa32df517"], + "sender": ["9c6249e03654", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -541,7 +541,7 @@ { "id": "speech-desktop-start-superseded.outer-refused-no-message:stale-start-cancelled", "observation": { - "sender": ["ba48be88195e", "73faa32df517"], + "sender": ["02e3f6a398b9", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -554,7 +554,7 @@ { "id": "speech-desktop-start-superseded.method-not-found:stale-start-cancelled", "observation": { - "sender": ["3c4f020193c4", "73faa32df517"], + "sender": ["f2166355786a", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -567,7 +567,7 @@ { "id": "speech-desktop-start-superseded.transport-rejection:stale-start-cancelled", "observation": { - "sender": ["0ec7faed0e3d", "73faa32df517"], + "sender": ["a1030fe3abd4", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", @@ -580,7 +580,7 @@ { "id": "speech-desktop-start-superseded.transport-rejection-no-message:stale-start-cancelled", "observation": { - "sender": ["50620ee89249", "73faa32df517"], + "sender": ["106b709a0579", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", diff --git a/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json b/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json index 42b1624f28b..0f3839c8ed0 100644 --- a/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json +++ b/mobile/rpc-foundation/goldens/speech-audio-chunk-acknowledged.json @@ -13,7 +13,7 @@ "ordinal": 2, "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.chunk\",\"params\":{\"dictationId\":\"dictation-1\",\"audioBase64\":\"ACVKb5S53gM=\",\"sampleRate\":16000}}" }, - "182109450de8": { + "99956eef8fff": { "name": "speech.dictation.chunk#1", "ordinal": 1, "args": [ @@ -32,7 +32,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -69,7 +69,7 @@ { "id": "acknowledged", "observation": { - "sender": ["182109450de8"], + "sender": ["99956eef8fff"], "payloads": ["12521d219054"], "settlements": { "chunk": "bc459c132276" diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json b/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json index 9aabbf4419c..4a7e325614f 100644 --- a/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json +++ b/mobile/rpc-foundation/goldens/speech-desktop-start-fulfilled.json @@ -4,7 +4,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "values": { - "3b5708754539": { + "4c42677aca21": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -21,7 +21,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -61,7 +61,7 @@ { "id": "recording", "observation": { - "sender": ["3b5708754539"], + "sender": ["4c42677aca21"], "payloads": ["a7ddd021010a"], "settlements": { "start": "84e5ca07cb7a" diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json b/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json index ca5159beb86..0ebe3392aec 100644 --- a/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json +++ b/mobile/rpc-foundation/goldens/speech-desktop-start-recording-failed.json @@ -11,41 +11,7 @@ "idle": true, "started": "unstarted" }, - "217153ab0a32": { - "name": "speech.dictation.cancel#1", - "ordinal": 4, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "dictation-1" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "cancelled": true - } - } - } - }, - "3b5708754539": { + "4c42677aca21": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -62,7 +28,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -79,6 +45,40 @@ } } }, + "4c97e2d0e296": { + "name": "speech.dictation.cancel#1", + "ordinal": 4, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "dictation-1" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "cancelled": true + } + } + } + }, "7a657475cacc": { "status": "rejected", "startedAt": 0, @@ -111,7 +111,7 @@ { "id": "rolled-back", "observation": { - "sender": ["3b5708754539", "217153ab0a32"], + "sender": ["4c42677aca21", "4c97e2d0e296"], "payloads": ["a7ddd021010a", "d6e61920633f"], "settlements": { "start": "7a657475cacc" diff --git a/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json b/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json index ec8e3ea12c4..7b5aad5f777 100644 --- a/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json +++ b/mobile/rpc-foundation/goldens/speech-desktop-start-superseded.json @@ -4,7 +4,7 @@ "family": "speech.dictation-start", "namedDeltas": [], "values": { - "3b5708754539": { + "4c42677aca21": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -21,7 +21,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -38,7 +38,13 @@ } } }, - "73faa32df517": { + "7ed3d39f0607": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": false + }, + "9ab91ee194b8": { "name": "speech.dictation.cancel#1", "ordinal": 3, "args": [ @@ -55,7 +61,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "failWhenDisconnected": true } } ], @@ -72,12 +78,6 @@ } } }, - "7ed3d39f0607": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": false - }, "a7ddd021010a": { "name": "speech.dictation.start#1", "ordinal": 2, @@ -110,7 +110,7 @@ { "id": "stale-start-cancelled", "observation": { - "sender": ["3b5708754539", "73faa32df517"], + "sender": ["4c42677aca21", "9ab91ee194b8"], "payloads": ["a7ddd021010a", "ce9cbc8837fa"], "settlements": { "supersede": "eb79a9b3682a", diff --git a/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json b/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json index 7db1cd929bb..c69d15c6603 100644 --- a/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json +++ b/mobile/rpc-foundation/goldens/speech-dictation-session-cancelled.json @@ -4,40 +4,6 @@ "family": "speech.dictation-session", "namedDeltas": [], "values": { - "422e902e66a5": { - "name": "speech.dictation.cancel#1", - "ordinal": 3, - "args": [ - { - "name": "method", - "value": "speech.dictation.cancel" - }, - { - "name": "params", - "value": { - "dictationId": "mobile-dictation-1767225600000-8ig2henseon" - } - }, - { - "name": "options", - "value": { - "$rpc": "absent" - } - } - ], - "settlement": { - "status": "fulfilled", - "startedAt": 0, - "settledAt": 0, - "value": { - "id": "frame-2", - "ok": true, - "result": { - "cancelled": true - } - } - } - }, "6b76435b6f3e": { "error": { "$rpc": "null" @@ -50,7 +16,7 @@ "ordinal": 2, "json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.start\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" }, - "aca401ac072e": { + "bd004568e24c": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -67,7 +33,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -96,6 +62,40 @@ "name": "speech.dictation.cancel#1", "ordinal": 4, "json": "{\"id\":\"frame-2\",\"deviceToken\":\"recording-device\",\"method\":\"speech.dictation.cancel\",\"params\":{\"dictationId\":\"mobile-dictation-1767225600000-8ig2henseon\"}}" + }, + "ed7a0ff994cd": { + "name": "speech.dictation.cancel#1", + "ordinal": 3, + "args": [ + { + "name": "method", + "value": "speech.dictation.cancel" + }, + { + "name": "params", + "value": { + "dictationId": "mobile-dictation-1767225600000-8ig2henseon" + } + }, + { + "name": "options", + "value": { + "failWhenDisconnected": true + } + } + ], + "settlement": { + "status": "fulfilled", + "startedAt": 0, + "settledAt": 0, + "value": { + "id": "frame-2", + "ok": true, + "result": { + "cancelled": true + } + } + } } }, "recording": { @@ -104,7 +104,7 @@ { "id": "cancelled", "observation": { - "sender": ["aca401ac072e", "422e902e66a5"], + "sender": ["bd004568e24c", "ed7a0ff994cd"], "payloads": ["859d310e63d6", "ebdac948a90d"], "settlements": { "mount": "eb79a9b3682a", diff --git a/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json b/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json index fe249575716..570afbc56d9 100644 --- a/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json +++ b/mobile/rpc-foundation/goldens/speech-dictation-session-transcript.json @@ -55,7 +55,7 @@ "status": "idle", "transcripts": ["hello world"] }, - "aca401ac072e": { + "bd004568e24c": { "name": "speech.dictation.start#1", "ordinal": 1, "args": [ @@ -72,7 +72,7 @@ { "name": "options", "value": { - "$rpc": "absent" + "timeoutMs": 30000 } } ], @@ -104,7 +104,7 @@ { "id": "transcribed", "observation": { - "sender": ["aca401ac072e", "4728ad10f92b"], + "sender": ["bd004568e24c", "4728ad10f92b"], "payloads": ["859d310e63d6", "1bdc94d60271"], "settlements": { "mount": "eb79a9b3682a", diff --git a/mobile/src/hooks/mobile-dictation-audio-chunk.ts b/mobile/src/hooks/mobile-dictation-audio-chunk.ts index 12a8eac1054..1ce5a1c7485 100644 --- a/mobile/src/hooks/mobile-dictation-audio-chunk.ts +++ b/mobile/src/hooks/mobile-dictation-audio-chunk.ts @@ -4,15 +4,101 @@ import { } from './mobile-dictation-pending-audio-budget' import { bytesToBase64 } from './mobile-dictation-session-state' import { dictationAudioChunkSend } from '../dictation/mobile-dictation-operations' -import type { DictationCaptureChunk } from '../platform/dictation-capture-contract' +import { MOBILE_DICTATION_RECONNECT_GRACE_MS } from '../../../src/shared/rpc-contract/speech-params' +import { isRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' +import { rpcPayloadMember } from '../transport/rpc-reader-payload' +import { waitForRpcClientReconnected } from '../transport/rpc-client-reconnect-wait' +import { isLogicalClientCutoverError } from '../transport/stable-logical-rpc-client' +import { isLogicalClientSuspendedError } from '../transport/logical-client-suspended-error' +import type { + DictationCapture, + DictationCaptureChunk +} from '../platform/dictation-capture-contract' import type { MobileDictationPendingAudioBudget } from './mobile-dictation-pending-audio-budget' import type { RpcClient } from '../transport/rpc-client' type MobileDictationAudioChunkQueue = { pendingChunks: Set> pendingAudioBudget: MobileDictationPendingAudioBudget - shouldReleaseBudget: (dictationId: string) => boolean + isActive: (dictationId: string) => boolean failActiveDictation: (dictationId: string, err: unknown) => void + transcriptSoFar: MobileDictationTranscriptSoFar +} + +// Hosts that keep dictation through a reconnect report the text so far; a failure hands it over instead of dropping it. +export class MobileDictationTranscriptSoFar { + private kept: { dictationId: string; text: string } | null = null + + note(dictationId: string, text: string): void { + this.kept = { dictationId, text } + } + + take(dictationId: string): string { + const kept = this.kept + this.kept = null + return kept?.dictationId === dictationId ? kept.text : '' + } +} + +// A request refused or still waiting while the connection moved never reached the host; send it again on the new one, within the host's grace. +export async function sendAcrossConnectionMove( + client: RpcClient, + send: () => Promise +): Promise { + try { + return await send() + } catch (err) { + if (isLogicalClientCutoverError(err) && !isRpcDeliveryUnknown(err)) { + return send() + } + if (isLogicalClientSuspendedError(err) && (await waitForReconnect(client))) { + return send() + } + throw err + } +} + +// Every chunk refused during one outage shares a single wait instead of a listener and timer each. +const reconnectWaits = new WeakMap>() + +function waitForReconnect(client: RpcClient): Promise { + let wait = reconnectWaits.get(client) + if (!wait) { + wait = waitForRpcClientReconnected(client, MOBILE_DICTATION_RECONNECT_GRACE_MS).finally(() => + reconnectWaits.delete(client) + ) + reconnectWaits.set(client, wait) + } + return wait +} + +export function subscribeMobileDictationAudioChunks( + capture: Pick, + source: { + getClient: () => RpcClient | null + getActiveId: () => string | null + isAccepting: () => boolean + }, + queue: Omit +): { remove: () => void } { + // Reuse wiring on the microphone hot path instead of allocating per chunk. + const chunkQueue = { ...queue, isActive: (id: string) => source.getActiveId() === id } + return capture.onChunk((chunk) => { + const client = source.getClient() + const dictationId = source.getActiveId() + if (!client || !dictationId || !source.isAccepting()) { + return + } + if (chunk.droppedBytes > 0) { + // Page capture loss and budget overflow both mean the page fell behind the microphone; report the same error. Native capture never drops here. + queue.failActiveDictation( + dictationId, + new Error(MOBILE_DICTATION_CONNECTION_SLOW_ERROR_MESSAGE) + ) + return + } + enqueueMobileDictationAudioChunk(client, dictationId, chunk, chunkQueue) + }) } export function enqueueMobileDictationAudioChunk( @@ -30,18 +116,33 @@ export function enqueueMobileDictationAudioChunk( ) return } - const sendChunk = dictationAudioChunkSend - .request(client, { - dictationId, - audioBase64: bytesToBase64(bytes), - sampleRate: MOBILE_DICTATION_PCM_SAMPLE_RATE + const params = { + dictationId, + audioBase64: bytesToBase64(bytes), + sampleRate: MOBILE_DICTATION_PCM_SAMPLE_RATE + } + const sendChunk = sendAcrossConnectionMove(client, () => + // Wait for a reconnect only as long as the host keeps the session; past that, fail and keep the text so far. + dictationAudioChunkSend.request(client, params, { + timeoutMs: MOBILE_DICTATION_RECONNECT_GRACE_MS }) + ) .then((reply) => { - dictationAudioChunkSend.interpret(reply) + // Only newer hosts send the text, and only when it changed; any other result still acknowledges the chunk. + const result = dictationAudioChunkSend.interpret(reply) + const text = result == null ? undefined : rpcPayloadMember(result, 'text') + if (typeof text === 'string' && queue.isActive(dictationId)) { + queue.transcriptSoFar.note(dictationId, text) + } + }) + .catch((err) => { + // The host keeps the session through a reconnect; only audio in flight at the drop is lost. + if (!isRpcDeliveryUnknown(err)) { + queue.failActiveDictation(dictationId, err) + } }) - .catch((err) => queue.failActiveDictation(dictationId, err)) .finally(() => { - if (queue.shouldReleaseBudget(dictationId)) { + if (queue.isActive(dictationId)) { queue.pendingAudioBudget.release(byteLength) } queue.pendingChunks.delete(sendChunk) diff --git a/mobile/src/hooks/mobile-dictation-desktop-start.test.ts b/mobile/src/hooks/mobile-dictation-desktop-start.test.ts index 9049ac725a9..4a6871b758d 100644 --- a/mobile/src/hooks/mobile-dictation-desktop-start.test.ts +++ b/mobile/src/hooks/mobile-dictation-desktop-start.test.ts @@ -72,9 +72,11 @@ describe('startMobileDictationDesktopSession', () => { expect(harness.setIdle).not.toHaveBeenCalled() expect(harness.getActiveId()).toBe('dictation-b') expect(harness.commitRecordingStart).not.toHaveBeenCalled() - expect(harness.sendRequest).toHaveBeenCalledWith('speech.dictation.cancel', { - dictationId: 'dictation-a' - }) + expect(harness.sendRequest).toHaveBeenCalledWith( + 'speech.dictation.cancel', + { dictationId: 'dictation-a' }, + { failWhenDisconnected: true } + ) }) it('returns to idle when a disable makes the start stale', async () => { @@ -112,9 +114,11 @@ describe('startMobileDictationDesktopSession', () => { // The hook opened capture before requesting a session; refusal must release it and the screen because no outer start catch does so. expect(harness.rollbackRecordingStart).toHaveBeenCalledOnce() - expect(harness.sendRequest).toHaveBeenCalledWith('speech.dictation.cancel', { - dictationId: 'dictation-a' - }) + expect(harness.sendRequest).toHaveBeenCalledWith( + 'speech.dictation.cancel', + { dictationId: 'dictation-a' }, + { failWhenDisconnected: true } + ) }) it('leaves the capture alone when the failure is no longer the current start', async () => { @@ -135,9 +139,11 @@ describe('startMobileDictationDesktopSession', () => { // Capture has no start identity; rolling back stale A would stop B’s microphone and release B’s screen. expect(harness.rollbackRecordingStart).not.toHaveBeenCalled() // A still owns its desktop session and must cancel that alone. - expect(harness.sendRequest).toHaveBeenCalledWith('speech.dictation.cancel', { - dictationId: 'dictation-a' - }) + expect(harness.sendRequest).toHaveBeenCalledWith( + 'speech.dictation.cancel', + { dictationId: 'dictation-a' }, + { failWhenDisconnected: true } + ) }) it('commits recording before returning a current start to the hook', async () => { @@ -160,9 +166,11 @@ describe('startMobileDictationDesktopSession', () => { 'Audio focus request failed' ) - expect(harness.sendRequest).toHaveBeenCalledWith('speech.dictation.cancel', { - dictationId: 'dictation-a' - }) + expect(harness.sendRequest).toHaveBeenCalledWith( + 'speech.dictation.cancel', + { dictationId: 'dictation-a' }, + { failWhenDisconnected: true } + ) expect(harness.getActiveId()).toBeNull() expect(harness.setIdle).toHaveBeenCalledOnce() expect(harness.rollbackRecordingStart).toHaveBeenCalledOnce() @@ -175,9 +183,11 @@ describe('startMobileDictationDesktopSession', () => { 'Failed to start microphone recording' ) - expect(harness.sendRequest).toHaveBeenCalledWith('speech.dictation.cancel', { - dictationId: 'dictation-a' - }) + expect(harness.sendRequest).toHaveBeenCalledWith( + 'speech.dictation.cancel', + { dictationId: 'dictation-a' }, + { failWhenDisconnected: true } + ) expect(harness.getActiveId()).toBeNull() expect(harness.setIdle).toHaveBeenCalledOnce() expect(harness.rollbackRecordingStart).toHaveBeenCalledOnce() diff --git a/mobile/src/hooks/mobile-dictation-desktop-start.ts b/mobile/src/hooks/mobile-dictation-desktop-start.ts index 9790d0fd389..79cdf356c9d 100644 --- a/mobile/src/hooks/mobile-dictation-desktop-start.ts +++ b/mobile/src/hooks/mobile-dictation-desktop-start.ts @@ -3,6 +3,7 @@ import { dictationSessionCancel, dictationSessionStart } from '../dictation/mobile-dictation-operations' +import { MOBILE_DICTATION_RECONNECT_GRACE_MS } from '../../../src/shared/rpc-contract/speech-params' import type { RpcClient } from '../transport/rpc-client' type StartMobileDictationDesktopSessionOptions = { @@ -18,6 +19,16 @@ type StartMobileDictationDesktopSessionOptions = { rollbackRecordingStart: () => void } +// Offline there is nothing to cancel against; the host drops a session its phone stops reaching, so never wait for a reconnect. +export function cancelDesktopDictation(client: RpcClient, dictationId: string): Promise { + return dictationSessionCancel + .request(client, { dictationId }, { failWhenDisconnected: true }) + .then( + () => undefined, + () => undefined + ) +} + function isCurrentStart(options: StartMobileDictationDesktopSessionOptions): boolean { return isCurrentMobileDictationStart( options.getCurrentGeneration(), @@ -42,7 +53,7 @@ async function cancelStaleStart(options: StartMobileDictationDesktopSessionOptio const { client, dictationId } = options options.clearActiveId(dictationId) setIdleIfGenerationCurrent(options) - await dictationSessionCancel.request(client, { dictationId }).catch(() => undefined) + await cancelDesktopDictation(client, dictationId) } export async function startMobileDictationDesktopSession( @@ -51,7 +62,12 @@ export async function startMobileDictationDesktopSession( const { client, dictationId } = options try { - const reply = await dictationSessionStart.request(client, { dictationId }) + // The mic is already open, so a start stalled by a drop must give up rather than hold it. + const reply = await dictationSessionStart.request( + client, + { dictationId }, + { timeoutMs: MOBILE_DICTATION_RECONNECT_GRACE_MS } + ) dictationSessionStart.interpret(reply) } catch (err) { const wasCurrent = isCurrentStart(options) @@ -64,7 +80,7 @@ export async function startMobileDictationDesktopSession( } } options.clearActiveId(dictationId) - await dictationSessionCancel.request(client, { dictationId }).catch(() => undefined) + await cancelDesktopDictation(client, dictationId) // Awaited cleanup may overlap a newer start; stale work must not reset or // report over the replacement session. const shouldReport = wasCurrent && canReportStartFailure(options) @@ -96,7 +112,7 @@ export async function startMobileDictationDesktopSession( // Continue releasing independently owned resources after native audio failure. } options.clearActiveId(dictationId) - await dictationSessionCancel.request(client, { dictationId }).catch(() => undefined) + await cancelDesktopDictation(client, dictationId) const shouldReport = wasCurrent && canReportStartFailure(options) setIdleIfGenerationCurrent(options) if (!shouldReport) { diff --git a/mobile/src/hooks/mobile-dictation-pending-audio-budget.test.ts b/mobile/src/hooks/mobile-dictation-pending-audio-budget.test.ts index 7f210226546..bd6564258fc 100644 --- a/mobile/src/hooks/mobile-dictation-pending-audio-budget.test.ts +++ b/mobile/src/hooks/mobile-dictation-pending-audio-budget.test.ts @@ -2,13 +2,13 @@ import { describe, expect, it } from 'vitest' import { MobileDictationPendingAudioBudget } from './mobile-dictation-pending-audio-budget' describe('MobileDictationPendingAudioBudget', () => { - it('caps pending raw PCM audio at five seconds', () => { + it('caps pending raw PCM audio at thirty seconds', () => { const budget = new MobileDictationPendingAudioBudget() - expect(budget.tryReserve(159_999)).toBe(true) + expect(budget.tryReserve(959_999)).toBe(true) expect(budget.tryReserve(1)).toBe(true) - expect(budget.pendingAudioBytes).toBe(160_000) + expect(budget.pendingAudioBytes).toBe(960_000) expect(budget.tryReserve(1)).toBe(false) - expect(budget.pendingAudioBytes).toBe(160_000) + expect(budget.pendingAudioBytes).toBe(960_000) }) it('releases completed chunks and clamps stale releases after reset', () => { diff --git a/mobile/src/hooks/mobile-dictation-pending-audio-budget.ts b/mobile/src/hooks/mobile-dictation-pending-audio-budget.ts index 7f814e493bc..c90d0f313c5 100644 --- a/mobile/src/hooks/mobile-dictation-pending-audio-budget.ts +++ b/mobile/src/hooks/mobile-dictation-pending-audio-budget.ts @@ -1,11 +1,17 @@ -export const MOBILE_DICTATION_PCM_SAMPLE_RATE = 16000 +import { + DICTATION_SAMPLE_RATE, + MOBILE_DICTATION_RECONNECT_GRACE_MS, + PCM_BYTES_PER_SAMPLE +} from '../../../src/shared/rpc-contract/speech-params' -const PCM_BYTES_PER_SAMPLE = 2 -const MAX_PENDING_AUDIO_SECONDS = 5 +export const MOBILE_DICTATION_PCM_SAMPLE_RATE = DICTATION_SAMPLE_RATE -// Why: dictation chunk RPCs can wait through reconnect before timing out, so +// Hold the speech captured while a reconnect is pending, for as long as the host keeps the session. +const MAX_PENDING_AUDIO_SECONDS = MOBILE_DICTATION_RECONNECT_GRACE_MS / 1000 + +// Why: dictation chunk RPCs wait through reconnect before timing out, so // cap retained raw microphone audio before it is expanded into base64 payloads. -export const MOBILE_DICTATION_MAX_PENDING_AUDIO_BYTES = +const MOBILE_DICTATION_MAX_PENDING_AUDIO_BYTES = MOBILE_DICTATION_PCM_SAMPLE_RATE * PCM_BYTES_PER_SAMPLE * MAX_PENDING_AUDIO_SECONDS export const MOBILE_DICTATION_CONNECTION_SLOW_ERROR_MESSAGE = diff --git a/mobile/src/hooks/mobile-dictation-reconnect.test.tsx b/mobile/src/hooks/mobile-dictation-reconnect.test.tsx new file mode 100644 index 00000000000..de777adac85 --- /dev/null +++ b/mobile/src/hooks/mobile-dictation-reconnect.test.tsx @@ -0,0 +1,183 @@ +// The host keeps a phone's dictation through a reconnect; the phone must not end it over the chunks the drop took with it. +import { createElement } from 'react' +import { act, create } from 'react-test-renderer' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + createFakeRpcClient, + type FakeRpcClient, + type SentRequest +} from '../mobile-web-shell/bridge-host-test-fakes' +import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity' +import { LogicalClientCutoverError } from '../transport/stable-logical-rpc-client' +import type { ConnectionState } from '../transport/types' +import type { + DictationCapture, + DictationCaptureChunk +} from '../platform/dictation-capture-contract' + +const seam = vi.hoisted(() => ({ + chunkHandlers: new Set<(chunk: DictationCaptureChunk) => void>() +})) + +vi.mock('react-native', () => ({ + AppState: { currentState: 'active', addEventListener: () => ({ remove: () => {} }) }, + Platform: { OS: 'ios' } +})) + +vi.mock('../platform/dictation-capture', () => { + const capture: DictationCapture = { + open: async () => ({ ok: true }), + begin: () => true, + end: async () => {}, + release: () => {}, + onChunk: (handler) => { + seam.chunkHandlers.add(handler) + return { remove: () => seam.chunkHandlers.delete(handler) } + }, + onInterruption: () => ({ remove: () => {} }) + } + return { useDictationCapture: () => capture } +}) + +import { useMobileDictation, type UseMobileDictationResult } from './use-mobile-dictation' + +type Reply = { text?: string } | Error + +const held: { dictation: UseMobileDictationResult | null } = { dictation: null } + +function mount(rpc: FakeRpcClient, transcripts: string[], errors: string[]): void { + function Probe(): null { + held.dictation = useMobileDictation({ + client: rpc, + enabled: true, + onTranscript: (text) => transcripts.push(text), + onError: (err) => errors.push(err.message) + }) + return null + } + act(() => { + create(createElement(Probe)) + }) +} + +function dictation(): UseMobileDictationResult { + if (held.dictation === null) { + throw new Error('nothing mounted') + } + return held.dictation +} + +// Answer chunks and finishes with the next scripted reply, and everything else with success. +async function pump( + rpc: FakeRpcClient, + sent: SentRequest[], + chunkReplies: Reply[], + finishReplies: Reply[] = [] +): Promise { + for (let round = 0; round < 8; round += 1) { + for (const request of rpc.requests.splice(0)) { + sent.push(request) + const reply: Reply = + request.method === 'speech.dictation.chunk' + ? (chunkReplies.shift() ?? {}) + : request.method === 'speech.dictation.finish' + ? (finishReplies.shift() ?? { text: 'hello world' }) + : {} + if (reply instanceof Error) { + request.reject(reply) + } else { + request.resolve({ id: 'desktop', ok: true, result: reply }) + } + } + await Promise.resolve() + await Promise.resolve() + } +} + +async function record(rpc: FakeRpcClient, sent: SentRequest[], chunkReplies: Reply[]) { + await act(async () => { + const started = dictation().start() + await pump(rpc, sent, []) + await started + }) + for (const reply of chunkReplies) { + await act(async () => { + for (const handler of seam.chunkHandlers) { + handler({ data: Uint8Array.from([1, 2]), droppedBytes: 0 }) + } + await pump(rpc, sent, [reply]) + }) + } +} + +const methodsOf = (sent: SentRequest[]) => sent.map((request) => request.method) + +beforeEach(() => { + seam.chunkHandlers.clear() + held.dictation = null +}) + +describe('dictation through a reconnect', () => { + it('keeps recording past chunks the drop lost, and resends requests that never left', async () => { + const rpc = createFakeRpcClient() + const sent: SentRequest[] = [] + const transcripts: string[] = [] + const errors: string[] = [] + mount(rpc, transcripts, errors) + + await record(rpc, sent, [ + { text: 'hello' }, + markRpcDeliveryUnknown(new Error('Connection interrupted')), + new LogicalClientCutoverError(new Error('Connection closed')) + ]) + + expect(dictation().status).toBe('recording') + // Each captured chunk went out once, and the one the cutover held back went out again. + expect(methodsOf(sent).filter((m) => m === 'speech.dictation.chunk')).toHaveLength(4) + await act(async () => { + const stopped = dictation().stop() + await pump(rpc, sent, [], [new LogicalClientCutoverError(new Error('Connection closed'))]) + await stopped + }) + expect(methodsOf(sent).filter((m) => m === 'speech.dictation.finish')).toHaveLength(2) + expect(methodsOf(sent)).not.toContain('speech.dictation.cancel') + expect(transcripts).toEqual(['hello world']) + expect(errors).toEqual([]) + }) + + it('holds a chunk refused while recovery dials a replacement, and sends it once connected', async () => { + let state: ConnectionState = 'connected' + const rpc = createFakeRpcClient({ getState: () => state }) + const sent: SentRequest[] = [] + const transcripts: string[] = [] + const errors: string[] = [] + mount(rpc, transcripts, errors) + + state = 'disconnected' + // The OTA page sees the shell's error rebuilt as another class with the same message. + await record(rpc, sent, [new Error('Client suspended')]) + expect(methodsOf(sent).filter((m) => m === 'speech.dictation.chunk')).toHaveLength(1) + await act(async () => { + state = 'connected' + rpc.pushState('connected') + await pump(rpc, sent, []) + }) + + expect(methodsOf(sent).filter((m) => m === 'speech.dictation.chunk')).toHaveLength(2) + expect(dictation().status).toBe('recording') + expect(errors).toEqual([]) + }) + + it('keeps what was transcribed when the connection does not come back', async () => { + const rpc = createFakeRpcClient() + const sent: SentRequest[] = [] + const transcripts: string[] = [] + const errors: string[] = [] + mount(rpc, transcripts, errors) + + await record(rpc, sent, [{ text: 'hello' }, new Error('Connection closed')]) + + expect(transcripts).toEqual(['hello']) + expect(errors).toEqual(['Connection closed']) + }) +}) diff --git a/mobile/src/hooks/use-mobile-dictation.ts b/mobile/src/hooks/use-mobile-dictation.ts index 33fcb05d4a3..e4de0b23a17 100644 --- a/mobile/src/hooks/use-mobile-dictation.ts +++ b/mobile/src/hooks/use-mobile-dictation.ts @@ -1,10 +1,11 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { useDictationCapture } from '../platform/dictation-capture' +import { MobileDictationPendingAudioBudget } from './mobile-dictation-pending-audio-budget' import { - MOBILE_DICTATION_CONNECTION_SLOW_ERROR_MESSAGE, - MobileDictationPendingAudioBudget -} from './mobile-dictation-pending-audio-budget' -import { enqueueMobileDictationAudioChunk } from './mobile-dictation-audio-chunk' + MobileDictationTranscriptSoFar, + sendAcrossConnectionMove, + subscribeMobileDictationAudioChunks +} from './mobile-dictation-audio-chunk' import { DICTATION_FINISH_TIMEOUT_MS, MOBILE_DICTATION_INPUT_CLOSED_ERROR_MESSAGE, @@ -12,11 +13,11 @@ import { createMobileDictationId, isCurrentMobileDictationFinish } from './mobile-dictation-session-state' -import { startMobileDictationDesktopSession } from './mobile-dictation-desktop-start' import { - dictationSessionCancel, - dictationSessionFinish -} from '../dictation/mobile-dictation-operations' + cancelDesktopDictation, + startMobileDictationDesktopSession +} from './mobile-dictation-desktop-start' +import { dictationSessionFinish } from '../dictation/mobile-dictation-operations' import { rpcPayloadMember } from '../transport/rpc-reader-payload' import type { DictationStatus, @@ -43,6 +44,7 @@ export function useMobileDictation(options: UseMobileDictationOptions): UseMobil const pendingAudioBudgetRef = useRef(new MobileDictationPendingAudioBudget()) const acceptingChunksRef = useRef(false) const generationRef = useRef(0) + const transcriptSoFarRef = useRef(new MobileDictationTranscriptSoFar()) useLayoutEffect(() => { // Native audio events can arrive before passive Effects flush, but refs @@ -89,7 +91,11 @@ export function useMobileDictation(options: UseMobileDictationOptions): UseMobil activeIdRef.current = null closeDictationAudio() if (client && dictationId) { - void dictationSessionCancel.request(client, { dictationId }).catch(() => undefined) + void cancelDesktopDictation(client, dictationId) + } + const kept = transcriptSoFarRef.current.take(dictationId) + if (kept) { + onTranscriptRef.current(kept) } reportError(err) }, @@ -97,28 +103,19 @@ export function useMobileDictation(options: UseMobileDictationOptions): UseMobil ) useEffect(() => { - // Reuse wiring on the microphone hot path instead of allocating per chunk. - const audioChunkQueue = { + const source = { + getClient: () => clientRef.current, + getActiveId: () => activeIdRef.current, + isAccepting: () => enabledRef.current && acceptingChunksRef.current + } + const sub = subscribeMobileDictationAudioChunks(capture, source, { pendingChunks: pendingChunksRef.current, pendingAudioBudget: pendingAudioBudgetRef.current, - shouldReleaseBudget: (id: string) => activeIdRef.current === id, - failActiveDictation - } - const sub = capture.onChunk((chunk) => { - const client = clientRef.current - const dictationId = activeIdRef.current - if (!client || !dictationId || !enabledRef.current || !acceptingChunksRef.current) { - return - } - if (chunk.droppedBytes > 0) { - // Page capture loss and budget overflow both mean the page fell behind the microphone; report the same error. Native capture never drops here. - failActiveDictation(dictationId, new Error(MOBILE_DICTATION_CONNECTION_SLOW_ERROR_MESSAGE)) - return - } - enqueueMobileDictationAudioChunk(client, dictationId, chunk, audioChunkQueue) + failActiveDictation, + transcriptSoFar: transcriptSoFarRef.current }) return () => sub.remove() - }, [capture, failActiveDictation, reportError]) + }, [capture, failActiveDictation]) const start = useCallback(async () => { const client = clientRef.current @@ -221,10 +218,12 @@ export function useMobileDictation(options: UseMobileDictationOptions): UseMobil return } const finished = dictationSessionFinish.interpret( - await dictationSessionFinish.request( - client, - { dictationId }, - { timeoutMs: DICTATION_FINISH_TIMEOUT_MS } + await sendAcrossConnectionMove(client, () => + dictationSessionFinish.request( + client, + { dictationId }, + { timeoutMs: DICTATION_FINISH_TIMEOUT_MS } + ) ) ) if ( @@ -269,7 +268,7 @@ export function useMobileDictation(options: UseMobileDictationOptions): UseMobil activeIdRef.current = null closeDictationAudio() if (client && dictationId) { - await dictationSessionCancel.request(client, { dictationId }).catch(() => undefined) + await cancelDesktopDictation(client, dictationId) } // The cancel round trip may overlap a new recording; stale completion must neither report over it nor reset it. if (generationRef.current !== generation) { @@ -313,9 +312,7 @@ export function useMobileDictation(options: UseMobileDictationOptions): UseMobil closeDictationAudio() capture.release() if (clientRef.current && dictationId) { - void dictationSessionCancel - .request(clientRef.current, { dictationId }) - .catch(() => undefined) + void cancelDesktopDictation(clientRef.current, dictationId) } } }, [capture, closeDictationAudio]) diff --git a/mobile/src/mobile-web-shell/bridge/bridge-audio-verbs.ts b/mobile/src/mobile-web-shell/bridge/bridge-audio-verbs.ts index 72424b049f5..d6ed1082889 100644 --- a/mobile/src/mobile-web-shell/bridge/bridge-audio-verbs.ts +++ b/mobile/src/mobile-web-shell/bridge/bridge-audio-verbs.ts @@ -1,11 +1,11 @@ import { z } from 'zod' -import { MOBILE_DICTATION_MAX_PENDING_AUDIO_BYTES } from '../../hooks/mobile-dictation-pending-audio-budget' +import { MAX_DICTATION_AUDIO_CHUNK_BYTES } from '../../../../src/shared/rpc-contract/speech-params' // The shell owns microphone PCM and its wake lock; the page owns dictation state/RPCs. Pull from the ring through existing request/reply rather than adding a push frame. // Keep verb grant segments lowercase: a camel-cased audio.readChunk would fail manifest grammar and reject the whole bundle. -// Use the page’s pending-audio budget; a larger shell ring would hand the page bytes its own queue refuses. -export const BRIDGE_AUDIO_RING_MAX_BYTES = MOBILE_DICTATION_MAX_PENDING_AUDIO_BYTES +// A full ring drain goes to the host as one chunk, so stay within its chunk cap; shipped shells validate maxBytes against this. +export const BRIDGE_AUDIO_RING_MAX_BYTES = MAX_DICTATION_AUDIO_CHUNK_BYTES // Base64 uses four chars per three bytes; a full ring drain fits the message cap. export const BRIDGE_AUDIO_READ_MAX_BASE64_CHARS = Math.ceil(BRIDGE_AUDIO_RING_MAX_BYTES / 3) * 4 diff --git a/mobile/src/platform/dictation-capture-bridge-budget.test.tsx b/mobile/src/platform/dictation-capture-bridge-budget.test.tsx index ddf6ac6dee4..6a79c27f10d 100644 --- a/mobile/src/platform/dictation-capture-bridge-budget.test.tsx +++ b/mobile/src/platform/dictation-capture-bridge-budget.test.tsx @@ -21,7 +21,10 @@ import { type BridgePortPair } from '../mobile-web-shell/bridge/bridge-port-pair-test-harness' import { MobileDictationPendingAudioBudget } from '../hooks/mobile-dictation-pending-audio-budget' -import { enqueueMobileDictationAudioChunk } from '../hooks/mobile-dictation-audio-chunk' +import { + enqueueMobileDictationAudioChunk, + MobileDictationTranscriptSoFar +} from '../hooks/mobile-dictation-audio-chunk' import { createNativeAudioCapture, type NativeAudioEngine } from './native-audio' import { createPageDictationCapture } from './dictation-capture.web' import { @@ -98,7 +101,8 @@ async function runDictation( enqueueMobileDictationAudioChunk(pair.client, 'dictation-1', chunk, { pendingChunks, pendingAudioBudget, - shouldReleaseBudget: () => true, + isActive: () => true, + transcriptSoFar: new MobileDictationTranscriptSoFar(), failActiveDictation: (_id, error) => { refusals.push(error instanceof Error ? error.message : String(error)) } diff --git a/mobile/src/session/MobileTerminalInputActions.tsx b/mobile/src/session/MobileTerminalInputActions.tsx index ffd650651a6..c9c30a0a892 100644 --- a/mobile/src/session/MobileTerminalInputActions.tsx +++ b/mobile/src/session/MobileTerminalInputActions.tsx @@ -45,6 +45,8 @@ export function MobileTerminalInputActions({ }: MobileTerminalInputActionsProps) { useMobileColorScheme() const dictationActive = dictation.isStarting || dictation.isRecording + // Dictation outlives a reconnect, so its stop and cancel must too; only starting needs the connection. + const micDisabled = !canSend && !dictationActive && !dictation.isProcessing return ( <> true, - canSend: resolveMobileTerminalInputGate({ - connState, - activeHandle, - activeSessionTabType: tab.type - }).canSend, + canSend: gate.canSend, + canCompose: gate.canCompose, liveInputEnabled: false, showToast: () => {}, resetLiveInputFocus: () => {} @@ -279,7 +282,8 @@ describe('dictation uses the active composer readiness', () => { expect(draft.text).toBe('Draft') }) - it('cancels an in-flight agent-chat start when the connection drops', async () => { + // The host keeps the session through a reconnect, so a drop is not the end of the input. + it('keeps a start that spans a connection drop, and records once it lands', async () => { const client = createFakeRpcClient() const options = { client, @@ -289,21 +293,14 @@ describe('dictation uses the active composer readiness', () => { } satisfies ProbeOptions await render(options) await act(async () => model().startDictation()) - const pendingStart = client.requests.shift() - expect(pendingStart?.method).toBe('speech.dictation.start') - await render({ ...options, connState: 'disconnected' }) - await reply(client, 'speech.dictation.cancel', {}) - await act(async () => - pendingStart?.resolve({ id: 'desktop', ok: true, result: { started: true } }) - ) - await reply(client, 'speech.dictation.cancel', {}) - expect(device.recording).toBe(false) - expect(draft.text).toBe('Draft') + await render({ ...options, connState: 'reconnecting' }) await render(options) - await start(client) + await reply(client, 'speech.dictation.start', { started: true }) + expect(model().dictation.status).toBe('recording') + expect(device.recording).toBe(true) }) - it('drops a late transcript after disconnecting, then starts again on reconnect', async () => { + it('delivers the transcript of a finish that spans a connection drop', async () => { const client = createFakeRpcClient() const options = { client, @@ -313,19 +310,14 @@ describe('dictation uses the active composer readiness', () => { } satisfies ProbeOptions await render(options) await start(client) + await render({ ...options, connState: 'reconnecting' }) + expect(model().dictation.status).toBe('recording') await act(async () => { void model().dictation.stop() }) - const pendingFinish = client.requests.shift() - expect(pendingFinish?.method).toBe('speech.dictation.finish') - await render({ ...options, connState: 'disconnected' }) - await reply(client, 'speech.dictation.cancel', {}) - await act(async () => - pendingFinish?.resolve({ id: 'desktop', ok: true, result: { text: 'late words' } }) - ) - expect(draft.text).toBe('Draft') - expect(device.recording).toBe(false) await render(options) - await start(client) + await reply(client, 'speech.dictation.finish', { text: 'words across the drop' }) + expect(client.requests).toEqual([]) + expect(draft.text).toBe('Draft words across the drop') }) }) diff --git a/mobile/src/session/mobile-dictation-disabled-controls.test.tsx b/mobile/src/session/mobile-dictation-disabled-controls.test.tsx index 436008fd4b4..0a7fa8c4162 100644 --- a/mobile/src/session/mobile-dictation-disabled-controls.test.tsx +++ b/mobile/src/session/mobile-dictation-disabled-controls.test.tsx @@ -40,3 +40,37 @@ it('removes the disabled microphone while keeping image attachment available', a ]) await act(async () => renderer.unmount()) }) + +it('keeps the microphone able to stop a recording while the connection is down', async () => { + const idle = { isStarting: false, isRecording: false, isProcessing: false } + const props = { + canSend: false, + isAttaching: false, + dictation: { ...idle, isRecording: true }, + dictationMode: 'toggle', + buttonStyle: {}, + activeButtonStyle: {}, + disabledButtonStyle: {}, + onAttachImage: vi.fn(), + onAttachFile: vi.fn(), + onDictationToggle: vi.fn(), + onDictationPressIn: vi.fn(), + onDictationPressOut: vi.fn(), + onDictationCancel: vi.fn() + } + let renderer!: ReturnType + await act(async () => { + renderer = create(createElement(MobileTerminalInputActions, props)) + }) + const mic = () => + renderer.root.find( + (node) => + String(node.type) === 'Pressable' && node.props.accessibilityLabel !== 'Attach a photo' + ) + expect(mic().props.disabled).toBe(false) + await act(async () => + renderer.update(createElement(MobileTerminalInputActions, { ...props, dictation: idle })) + ) + expect(mic().props.disabled).toBe(true) + await act(async () => renderer.unmount()) +}) diff --git a/mobile/src/session/mobile-dictation-mode-refresh.test.tsx b/mobile/src/session/mobile-dictation-mode-refresh.test.tsx index 5828f839497..640123a7b5d 100644 --- a/mobile/src/session/mobile-dictation-mode-refresh.test.tsx +++ b/mobile/src/session/mobile-dictation-mode-refresh.test.tsx @@ -142,6 +142,7 @@ function Probe() { deviceTokenRef, flushPendingLiveInputBeforeExternalSend: async () => true, canSend: env.connected, + canCompose: true, liveInputEnabled: false, showToast: () => {}, resetLiveInputFocus: () => {} diff --git a/mobile/src/session/mobile-dictation-session-outcomes.test.tsx b/mobile/src/session/mobile-dictation-session-outcomes.test.tsx index 9fcd6163972..916a06a0637 100644 --- a/mobile/src/session/mobile-dictation-session-outcomes.test.tsx +++ b/mobile/src/session/mobile-dictation-session-outcomes.test.tsx @@ -126,6 +126,7 @@ function mount( activeHandleRef: useRef('t1'), flushPendingLiveInputBeforeExternalSend: async () => true, canSend: true, + canCompose: true, liveInputEnabled: options.liveInput, showToast: (message: string) => { seen.toasts.push(message) diff --git a/mobile/src/session/use-mobile-session-native-chat-dictation.ts b/mobile/src/session/use-mobile-session-native-chat-dictation.ts index 48e53a7c450..39e09bb6526 100644 --- a/mobile/src/session/use-mobile-session-native-chat-dictation.ts +++ b/mobile/src/session/use-mobile-session-native-chat-dictation.ts @@ -51,6 +51,7 @@ export function useMobileSessionNativeChatDictation( activeSessionTab, flushPendingLiveInputBeforeExternalSend, canSend, + canCompose, liveInputEnabled, showToast, resetLiveInputFocus @@ -129,10 +130,11 @@ export function useMobileSessionNativeChatDictation( } const chatDictationDestination = useRef<((text: string) => boolean | void) | undefined>(undefined) - const dictationTargetReady = showNativeChat ? connState === 'connected' : canSend + // Starting needs the host, but a reconnect must not end a recording: the host keeps the session through it. + const dictationCanStart = showNativeChat ? connState === 'connected' : canSend const dictation = useMobileDictation({ client, - enabled: dictationTargetReady && dictationEnabled, + enabled: (showNativeChat || canCompose) && dictationEnabled, onTranscript: (text) => { // Route dictation to the visible chat composer locally; terminal mode retains live-input routing. if (showNativeChatRef.current || chatDictationDestination.current) { @@ -185,6 +187,9 @@ export function useMobileSessionNativeChatDictation( const dictationNeedsSetupRef = useRef(false) const startDictation = useCallback(() => { + if (!dictationCanStart) { + return + } if (dictationNeedsSetupRef.current) { // Explain setup before the OS microphone prompt: mic access cannot help until the host can transcribe. setShowDictationSetup(true) @@ -204,6 +209,7 @@ export function useMobileSessionNativeChatDictation( }, [ activeHandle, dictation, + dictationCanStart, liveInputTerminalHandles, reportDictationFailure, setShowDictationSetup diff --git a/mobile/src/test-support/rpc-recording/adapters/dictation-mount-adapters.ts b/mobile/src/test-support/rpc-recording/adapters/dictation-mount-adapters.ts index 5d23e6a7b41..a32c512d1ef 100644 --- a/mobile/src/test-support/rpc-recording/adapters/dictation-mount-adapters.ts +++ b/mobile/src/test-support/rpc-recording/adapters/dictation-mount-adapters.ts @@ -1,6 +1,7 @@ import type { MountAdapter } from '../recording-scenario' import { hookMount, performHookAction } from '../hook-mount' import type { operationModuleLoader } from '../operation-module-loader' +import type * as AudioChunkModule from '../../../hooks/mobile-dictation-audio-chunk' const DICTATION_ID = 'dictation-1' const MODEL_ID = 'whisper-small' @@ -82,9 +83,10 @@ export function dictationMountAdapters( } }, 'speech.audio-chunk': ({ client, effect }) => { - const enqueue = modules.load( + const chunks = modules.load( 'mobile/src/hooks/mobile-dictation-audio-chunk.ts' - ).enqueueMobileDictationAudioChunk + ) + const enqueue = chunks.enqueueMobileDictationAudioChunk const budget = modules.load< typeof import('../../../hooks/mobile-dictation-pending-audio-budget') >('mobile/src/hooks/mobile-dictation-pending-audio-budget.ts') @@ -105,7 +107,8 @@ export function dictationMountAdapters( { pendingChunks, pendingAudioBudget, - shouldReleaseBudget: () => true, + isActive: () => true, + transcriptSoFar: new chunks.MobileDictationTranscriptSoFar(), failActiveDictation: (id: string, error: unknown) => { failures.push(error instanceof Error ? error.message : String(error)) effect('dictation-failed', { id }) diff --git a/mobile/src/transport/logical-client-suspended-error.ts b/mobile/src/transport/logical-client-suspended-error.ts new file mode 100644 index 00000000000..dea40f896ee --- /dev/null +++ b/mobile/src/transport/logical-client-suspended-error.ts @@ -0,0 +1,14 @@ +// A suspended client refuses before anything is written, while recovery dials a replacement. +export class LogicalClientSuspendedError extends Error { + constructor() { + super('Client suspended') + } +} + +// Why: errors crossing the page bridge are rebuilt as other classes, so also match by message. +export function isLogicalClientSuspendedError(error: unknown): boolean { + return ( + error instanceof LogicalClientSuspendedError || + (error instanceof Error && error.message === 'Client suspended') + ) +} diff --git a/mobile/src/transport/mobile-relay-connect-waiters.ts b/mobile/src/transport/mobile-relay-connect-waiters.ts new file mode 100644 index 00000000000..8015d74534f --- /dev/null +++ b/mobile/src/transport/mobile-relay-connect-waiters.ts @@ -0,0 +1,51 @@ +import type { ConnectionState } from './types' + +type RelaySessionStateSource = { + getState(): ConnectionState + onStateChange(listener: (state: ConnectionState) => void): () => void +} + +// Requests issued before a relay session connects wait here until it connects, fails, times out or is retired. +export class MobileRelayConnectWaiters { + private readonly releases = new Set<() => void>() + + constructor(private readonly session: RelaySessionStateSource) {} + + wait(timeoutMs: number): Promise { + if (this.session.getState() === 'connected') { + return Promise.resolve() + } + return new Promise((resolve, reject) => { + const settle = (error: Error | null): void => { + clearTimeout(timer) + unsubscribe() + this.releases.delete(release) + if (error) { + reject(error) + } else { + resolve() + } + } + const release = () => settle(new Error('relay session closed')) + this.releases.add(release) + const unsubscribe = this.session.onStateChange((next) => { + if (next === 'connected') { + settle(null) + } else if (next === 'disconnected' || next === 'auth-failed') { + settle(new Error(`relay session ${next}`)) + } + }) + const timer = setTimeout( + () => settle(new Error('relay session connection timed out')), + timeoutMs + ) + }) + } + + // A failed session already published disconnected, so requests issued since are still waiting; retiring it must release them to the replacement. + releaseAll(): void { + for (const release of this.releases) { + release() + } + } +} diff --git a/mobile/src/transport/mobile-relay-rpc-session.test.ts b/mobile/src/transport/mobile-relay-rpc-session.test.ts index 0f58b87b020..b58e0f0415e 100644 --- a/mobile/src/transport/mobile-relay-rpc-session.test.ts +++ b/mobile/src/transport/mobile-relay-rpc-session.test.ts @@ -326,6 +326,20 @@ describe('mobile relay RPC session', () => { expect(session.getState()).toBe('disconnected') }) + it('releases requests waiting on a failed session once it is retired, unsent', async () => { + const { session } = await authenticateSession() + fakes.linkOptions!.onError(new Error('relay lost')) + vi.useFakeTimers() + const settled = vi.fn() + session.sendRequest('status.get').catch(settled) + session.close() + // No timer advances: the request must not sit out its connect timeout. + await vi.waitFor(() => expect(settled).toHaveBeenCalledOnce()) + + expect(isRpcDeliveryUnknown(settled.mock.calls[0]![0])).toBe(false) + expect(fakes.sendText).not.toHaveBeenCalled() + }) + it('keeps a synchronous pre-write relay failure definite', async () => { const { session } = await authenticateSession() fakes.sendText.mockImplementationOnce(() => { diff --git a/mobile/src/transport/mobile-relay-rpc-session.ts b/mobile/src/transport/mobile-relay-rpc-session.ts index 911bb59f889..9ac1d971763 100644 --- a/mobile/src/transport/mobile-relay-rpc-session.ts +++ b/mobile/src/transport/mobile-relay-rpc-session.ts @@ -16,6 +16,7 @@ import { openRpcRequestBudget, resolvePostConnectRequestTimeout } from './rpc-re import { isRpcResponse } from './rpc-response-shape' import { RelayDialStageTracker, type RelayDialStageSource } from './relay-dial-stage' import { RelayPendingRequests } from './relay-pending-requests' +import { MobileRelayConnectWaiters } from './mobile-relay-connect-waiters' import { RpcSessionLivenessWatchdog } from './rpc-session-liveness-watchdog' import { settleMobileRuntimeCapabilities } from './mobile-runtime-capability-negotiation' import type { RelayHostCloseReason } from '../../../src/shared/relay-host-close-reason' @@ -138,6 +139,7 @@ export function connectMobileRelayRpcSession(args: { } }, close() { + connectWaiters.releaseAll() if (closed) { return } @@ -157,6 +159,7 @@ export function connectMobileRelayRpcSession(args: { probeAfterBackground: () => state === 'connected' && livenessWatchdog.probeNow(livenessIdentity, RELAY_RESUME_PROBE_MS) } + const connectWaiters = new MobileRelayConnectWaiters(client) const livenessWatchdog = new RpcSessionLivenessWatchdog({ transport: 'relay', idleProbeMs: null, @@ -265,31 +268,7 @@ export function connectMobileRelayRpcSession(args: { } function waitForConnected(timeoutMs = requestTimeoutMs): Promise { - if (state === 'connected') { - return Promise.resolve() - } - return new Promise((resolve, reject) => { - let timer: ReturnType | null = null - const unsubscribe = client.onStateChange((next) => { - if (next === 'connected') { - finish() - resolve() - } else if (next === 'disconnected' || next === 'auth-failed') { - finish() - reject(new Error(`relay session ${next}`)) - } - }) - timer = setTimeout(() => { - finish() - reject(new Error('relay session connection timed out')) - }, timeoutMs) - function finish(): void { - if (timer) { - clearTimeout(timer) - } - unsubscribe() - } - }) + return connectWaiters.wait(timeoutMs) } function publishState(next: ConnectionState): void { diff --git a/mobile/src/transport/stable-logical-rpc-client.test.ts b/mobile/src/transport/stable-logical-rpc-client.test.ts index f6bd60ba87f..e20866d615e 100644 --- a/mobile/src/transport/stable-logical-rpc-client.test.ts +++ b/mobile/src/transport/stable-logical-rpc-client.test.ts @@ -8,6 +8,7 @@ import { LogicalClientCutoverError, type MobileConnectionPath } from './stable-logical-rpc-client' +import { LogicalClientSuspendedError } from './logical-client-suspended-error' class FakeSession implements RpcClient { readonly sendRequest = @@ -169,6 +170,23 @@ describe('stable logical RPC client', () => { await expect(client.sendRequest('status.get')).resolves.toEqual(success('next')) }) + it('reports requests the suspended session never sent as suspended, and written ones as ambiguous', async () => { + const session = new FakeSession('connected') + const unsent = deferred() + const written = deferred() + session.sendRequest.mockReturnValueOnce(unsent.promise).mockReturnValueOnce(written.promise) + const client = createStableLogicalRpcClient(session, 'relay') + const waiting = client.sendRequest('speech.dictation.chunk').catch((error: unknown) => error) + const inFlight = client.sendRequest('speech.dictation.chunk').catch((error: unknown) => error) + + client.suspendActiveSession() + unsent.reject(new Error('relay session closed')) + written.reject(markRpcDeliveryUnknown(new Error('Client closed'))) + + expect(await waiting).toBeInstanceOf(LogicalClientSuspendedError) + expect(isRpcDeliveryUnknown(await inFlight)).toBe(true) + }) + it('preserves delivery ambiguity without replaying a mutation after relay replacement', async () => { const session = new FakeSession('connected') const replacement = new FakeSession('connected') diff --git a/mobile/src/transport/stable-logical-rpc-client.ts b/mobile/src/transport/stable-logical-rpc-client.ts index c00def9cacb..fab0b476d82 100644 --- a/mobile/src/transport/stable-logical-rpc-client.ts +++ b/mobile/src/transport/stable-logical-rpc-client.ts @@ -9,6 +9,7 @@ import { projectMobileRpcRequestParams } from './mobile-rpc-request-projection' import { LogicalClientConnectionPath } from './logical-client-connection-path' import type { RelayHostReachability } from './relay-host-reachability' import { isRpcDeliveryUnknown, markRpcDeliveryUnknown } from './rpc-delivery-ambiguity' +import { LogicalClientSuspendedError } from './logical-client-suspended-error' export type MobileConnectionPath = 'lan' | 'tailscale' | 'relay' @@ -87,7 +88,7 @@ export function createStableLogicalRpcClient( return Promise.reject(new Error('Client closed')) } if (suspended) { - return Promise.reject(new Error('Client suspended')) + return Promise.reject(new LogicalClientSuspendedError()) } const requestGeneration = generation const session = activeSession @@ -103,7 +104,12 @@ export function createStableLogicalRpcClient( (error: unknown) => { // Keep the physical error as cause: only that layer knows whether the frame reached the wire. reject( - requestGeneration !== generation ? new LogicalClientCutoverError(error) : error + requestGeneration !== generation + ? new LogicalClientCutoverError(error) + : // Suspension retires the session under requests it never sent. + suspended && !isRpcDeliveryUnknown(error) + ? new LogicalClientSuspendedError() + : error ) } ) diff --git a/src/main/runtime/github-check-details-paired.integration.test.ts b/src/main/runtime/github-check-details-paired.integration.test.ts index 6bc0625afaf..795db7e1e87 100644 --- a/src/main/runtime/github-check-details-paired.integration.test.ts +++ b/src/main/runtime/github-check-details-paired.integration.test.ts @@ -112,7 +112,6 @@ describe('paired check details over real encrypted loopback transport', () => { getRuntimeId: () => 'paired-check-host', getStartedAt: () => 1, cleanupSubscriptionsForConnection: () => {}, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, getRepoPRCheckDetails: (...args: Parameters) => query.getRepoPRCheckDetails(...args).finally(() => { diff --git a/src/main/runtime/remote-runtime-close-intent.integration.test.ts b/src/main/runtime/remote-runtime-close-intent.integration.test.ts index 59122de21aa..bc2d4738a5d 100644 --- a/src/main/runtime/remote-runtime-close-intent.integration.test.ts +++ b/src/main/runtime/remote-runtime-close-intent.integration.test.ts @@ -49,7 +49,6 @@ it( getRuntimeId: () => 'close-intent-runtime-test', getStartedAt: () => 1, cleanupSubscriptionsForConnection: () => {}, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, listMobileSessionTabs, refuseUnattributedMobileSessionTabClose, diff --git a/src/main/runtime/remote-runtime-request-connection.integration.test.ts b/src/main/runtime/remote-runtime-request-connection.integration.test.ts index dd6fae87919..9f48eee6f2a 100644 --- a/src/main/runtime/remote-runtime-request-connection.integration.test.ts +++ b/src/main/runtime/remote-runtime-request-connection.integration.test.ts @@ -49,7 +49,6 @@ describe('remote runtime request connection integration', () => { getRuntimeId: () => 'fetch-runtime-test', getStartedAt: () => 1, cleanupSubscriptionsForConnection: () => {}, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, listVisibleRepos: () => repos } as unknown as OrcaRuntimeService @@ -137,7 +136,6 @@ describe('remote runtime request connection integration', () => { subscriptionCleanups.get(id)?.() subscriptionCleanups.delete(id) }, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, showRepo: (selector: string) => { if (selector !== repo.id && selector !== `id:${repo.id}`) { @@ -305,7 +303,6 @@ describe('remote runtime request connection integration', () => { subscriptionCleanups.get(id)?.() subscriptionCleanups.delete(id) }, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, onClientEvent: (listener: (event: RuntimeClientEvent) => void) => { clientEventListeners.add(listener) @@ -553,7 +550,6 @@ describe('remote runtime request connection integration', () => { } } }, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, onClientEvent: (listener: (event: RuntimeClientEvent) => void) => { clientEventListeners.add(listener) diff --git a/src/main/runtime/rpc/methods/speech.test.ts b/src/main/runtime/rpc/methods/speech.test.ts index 2ef23c69762..d812febe0d1 100644 --- a/src/main/runtime/rpc/methods/speech.test.ts +++ b/src/main/runtime/rpc/methods/speech.test.ts @@ -30,8 +30,7 @@ describe('speech RPC methods', () => { dictationId: 'dict-1', audioBase64: 'AAAA', sampleRate: 16_000, - clientId: undefined, - connectionId: undefined + clientId: undefined }) }) diff --git a/src/main/runtime/rpc/methods/speech.ts b/src/main/runtime/rpc/methods/speech.ts index 0d8da9b6de9..79bff553a38 100644 --- a/src/main/runtime/rpc/methods/speech.ts +++ b/src/main/runtime/rpc/methods/speech.ts @@ -42,29 +42,28 @@ export const SPEECH_METHODS = [ name: 'speech.dictation.start', permission: 'workspace', params: DictationStart, - handler: async (params, { runtime, clientId, connectionId }) => - runtime.startMobileDictation({ ...params, clientId, connectionId }) + handler: async (params, { runtime, clientId }) => + runtime.startMobileDictation({ ...params, clientId }) }), defineMethod({ name: 'speech.dictation.chunk', permission: 'workspace', params: DictationChunk, - handler: (params, { runtime, clientId, connectionId }) => - runtime.feedMobileDictation({ ...params, clientId, connectionId }) + handler: (params, { runtime, clientId }) => runtime.feedMobileDictation({ ...params, clientId }) }), defineMethod({ name: 'speech.dictation.finish', permission: 'workspace', params: DictationHandle, - handler: async (params, { runtime, clientId, connectionId }) => - runtime.finishMobileDictation({ ...params, clientId, connectionId }) + handler: async (params, { runtime, clientId }) => + runtime.finishMobileDictation({ ...params, clientId }) }), defineMethod({ name: 'speech.dictation.cancel', permission: 'workspace', params: DictationHandle, - handler: async (params, { runtime, clientId, connectionId }) => - runtime.cancelMobileDictation({ ...params, clientId, connectionId }) + handler: async (params, { runtime, clientId }) => + runtime.cancelMobileDictation({ ...params, clientId }) }), defineMethod({ name: 'speech.openAiKey.status', diff --git a/src/main/runtime/runtime-mobile-dictation-controller.test.ts b/src/main/runtime/runtime-mobile-dictation-controller.test.ts new file mode 100644 index 00000000000..e13664b3e87 --- /dev/null +++ b/src/main/runtime/runtime-mobile-dictation-controller.test.ts @@ -0,0 +1,76 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { getDefaultVoiceSettings } from '../../shared/constants' +import { + setSpeechServiceFactories, + type SpeechServiceFactories +} from '../speech/speech-runtime-service' +import { MOBILE_DICTATION_RECONNECT_GRACE_MS } from '../../shared/rpc-contract/speech-params' +import { RuntimeMobileDictationController } from './runtime-mobile-dictation-controller' +import type { RuntimeStore } from './runtime-store-contract' + +type Sink = (event: { type: string; text?: string }) => void + +let sink: Sink = () => {} +const stopDictation = vi.fn(async () => {}) + +beforeEach(() => { + vi.useFakeTimers() + stopDictation.mockClear() + const factories = { + createModelManager: () => ({ getModelState: async () => ({ status: 'ready' }) }), + createSttService: () => ({ + startDictation: async (_modelId: string, next: Sink) => { + sink = next + }, + feedAudio: () => {}, + stopDictation + }) + } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the controller calls only getModelState, startDictation, feedAudio and stopDictation. + setSpeechServiceFactories(factories as unknown as SpeechServiceFactories) +}) + +afterEach(() => { + setSpeechServiceFactories(null) + vi.useRealTimers() +}) + +async function startPhoneDictation(): Promise { + const store = { getSettings: () => ({ voice: { ...getDefaultVoiceSettings(), enabled: true } }) } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the controller reads only getSettings. + const controller = new RuntimeMobileDictationController(() => store as unknown as RuntimeStore) + await controller.start({ dictationId: 'd-1', modelId: 'test-model', clientId: 'phone-a' }) + sink({ type: 'final', text: 'before the drop' }) + return controller +} + +const chunk = { dictationId: 'd-1', audioBase64: 'AAAA', sampleRate: 16_000, clientId: 'phone-a' } + +describe('mobile dictation across a phone reconnect', () => { + it('keeps the session while the phone keeps reaching it, whatever connection it uses', async () => { + const controller = await startPhoneDictation() + + await vi.advanceTimersByTimeAsync(MOBILE_DICTATION_RECONNECT_GRACE_MS - 1) + expect(controller.feed(chunk)).toEqual({ dictationId: 'd-1', text: 'before the drop' }) + expect(controller.feed(chunk)).toEqual({ dictationId: 'd-1' }) + // A reply carrying the text may be lost at a drop, so it is sent again a second later. + await vi.advanceTimersByTimeAsync(1000) + expect(controller.feed(chunk)).toEqual({ dictationId: 'd-1', text: 'before the drop' }) + await vi.advanceTimersByTimeAsync(MOBILE_DICTATION_RECONNECT_GRACE_MS - 1) + sink({ type: 'final', text: 'after it' }) + + await expect(controller.finish({ dictationId: 'd-1', clientId: 'phone-a' })).resolves.toEqual({ + dictationId: 'd-1', + text: 'before the drop after it' + }) + }) + + it('releases the speech model when the phone goes quiet for the grace', async () => { + const controller = await startPhoneDictation() + + await vi.advanceTimersByTimeAsync(MOBILE_DICTATION_RECONNECT_GRACE_MS) + + expect(stopDictation).toHaveBeenCalledOnce() + expect(() => controller.feed(chunk)).toThrow('dictation_stream_not_started') + }) +}) diff --git a/src/main/runtime/runtime-mobile-dictation-controller.ts b/src/main/runtime/runtime-mobile-dictation-controller.ts index b7da6af0acd..99d3c170870 100644 --- a/src/main/runtime/runtime-mobile-dictation-controller.ts +++ b/src/main/runtime/runtime-mobile-dictation-controller.ts @@ -1,4 +1,5 @@ import { getDefaultVoiceSettings } from '../../shared/constants' +import { MOBILE_DICTATION_RECONNECT_GRACE_MS } from '../../shared/rpc-contract/speech-params' import { getSpeechModelManager, getSpeechSttService } from '../speech/speech-runtime-service' import { getCatalogModel, isLocalSpeechModel } from '../speech/model-catalog' import { @@ -10,12 +11,16 @@ import type { RuntimeStore } from './runtime-store-contract' type MobileDictationSession = { id: string owner: string - clientId?: string - connectionId?: string + clientId: string state: 'starting' | 'active' | 'closing' partialText: string finalTexts: string[] errors: string[] + // Bumped by each transcript event; chunks arrive far more often, so replies carry text only after a bump or a second. + textVersion: number + reportedVersion: number + reportedAt: number + abandonTimer?: ReturnType } export class RuntimeMobileDictationController { @@ -27,7 +32,6 @@ export class RuntimeMobileDictationController { dictationId: string modelId?: string clientId?: string - connectionId?: string }): Promise<{ dictationId: string; modelId: string }> { const store = this.requireStore() const voice = store.getSettings().voice ?? getDefaultVoiceSettings() @@ -49,6 +53,10 @@ export class RuntimeMobileDictationController { if (!params.clientId) { throw new Error('dictation_requires_mobile_client') } + if (this.session?.clientId === params.clientId && this.session.state === 'active') { + // A phone runs one dictation at a time, so its new start retires one it lost track of (app relaunch inside the grace). + await this.stopSession(this.session) + } if (this.session) { throw new Error('dictation_already_active') } @@ -58,11 +66,13 @@ export class RuntimeMobileDictationController { id: params.dictationId, owner, clientId: params.clientId, - connectionId: params.connectionId, state: 'starting', partialText: '', finalTexts: [], - errors: [] + errors: [], + textVersion: 0, + reportedVersion: 0, + reportedAt: 0 } try { await getSpeechSttService(store).startDictation( @@ -75,6 +85,12 @@ export class RuntimeMobileDictationController { throw new Error('dictation_canceled') } this.session.state = 'active' + const session = this.session + // Owned by the phone, not its socket; it streams continuously, so this long without a request means it is gone. + session.abandonTimer = setTimeout( + () => void this.stopSession(session).catch(() => undefined), + MOBILE_DICTATION_RECONNECT_GRACE_MS + ) } catch (error) { if (this.session?.id === params.dictationId) { this.session = null @@ -89,8 +105,7 @@ export class RuntimeMobileDictationController { audioBase64: string sampleRate: number clientId?: string - connectionId?: string - }): { dictationId: string } { + }): { dictationId: string; text?: string } { const session = this.requireOwnedSession(params) if (session.state !== 'active') { throw new Error('dictation_stream_closing') @@ -104,68 +119,39 @@ export class RuntimeMobileDictationController { samples[i] = pcm.readInt16LE(i * 2) / 32768 } getSpeechSttService(this.requireStore()).feedAudio(samples, params.sampleRate, session.owner) - return { dictationId: params.dictationId } + // The text so far lets the phone keep it if the connection never comes back; resent each second in case a reply was lost. + const now = Date.now() + if (session.reportedVersion === session.textVersion && now - session.reportedAt < 1000) { + return { dictationId: params.dictationId } + } + session.reportedVersion = session.textVersion + session.reportedAt = now + return { dictationId: params.dictationId, text: transcriptOf(session) } } async finish(params: { dictationId: string clientId?: string - connectionId?: string }): Promise<{ dictationId: string; text: string }> { const session = this.requireOwnedSession(params) - session.state = 'closing' - try { - await getSpeechSttService(this.requireStore()).stopDictation(session.owner) - if (session.errors.length > 0) { - throw new Error(session.errors[0]) - } - return { - dictationId: params.dictationId, - text: [...session.finalTexts, session.partialText].join(' ').trim() - } - } finally { - if (this.session?.id === session.id) { - this.session = null - } + await this.stopSession(session) + if (session.errors.length > 0) { + throw new Error(session.errors[0]) } + return { dictationId: params.dictationId, text: transcriptOf(session) } } async cancel(params: { dictationId: string clientId?: string - connectionId?: string }): Promise<{ dictationId: string }> { const session = this.session - if ( - session?.id === params.dictationId && - params.clientId && - session.clientId === params.clientId && - (!session.connectionId || session.connectionId === params.connectionId) - ) { - session.state = 'closing' - try { - await getSpeechSttService(this.requireStore()).stopDictation(session.owner) - } finally { - if (this.session?.id === session.id) { - this.session = null - } - } + if (session?.id === params.dictationId && session.clientId === params.clientId) { + await this.stopSession(session) } return { dictationId: params.dictationId } } - cancelForConnection(connectionId: string): void { - if (this.session?.connectionId === connectionId) { - this.cancelSession(this.session) - } - } - - cancelForClient(clientId: string): void { - if (this.session?.clientId === clientId) { - this.cancelSession(this.session) - } - } - private acceptEvent( dictationId: string, event: { type: string; text?: string; error?: string } @@ -176,11 +162,13 @@ export class RuntimeMobileDictationController { } if (event.type === 'partial') { session.partialText = event.text ?? '' + session.textVersion += 1 } else if (event.type === 'final') { const text = event.text?.trim() if (text) { session.finalTexts.push(text) session.partialText = '' + session.textVersion += 1 } } else if (event.type === 'error') { session.errors.push(event.error ?? 'Speech worker error') @@ -190,33 +178,30 @@ export class RuntimeMobileDictationController { private requireOwnedSession(params: { dictationId: string clientId?: string - connectionId?: string }): MobileDictationSession { const session = this.session if (!session || session.id !== params.dictationId) { throw new Error('dictation_stream_not_started') } - if (!params.clientId || session.clientId !== params.clientId) { + if (session.clientId !== params.clientId) { throw new Error('dictation_owner_mismatch') } - if (session.connectionId && session.connectionId !== params.connectionId) { - throw new Error('dictation_owner_mismatch') + if (session.state === 'active') { + session.abandonTimer?.refresh() } return session } - private cancelSession(session: MobileDictationSession): void { - if (session.state === 'closing') { - return - } + private async stopSession(session: MobileDictationSession): Promise { + clearTimeout(session.abandonTimer) session.state = 'closing' - void getSpeechSttService(this.requireStore()) - .stopDictation(session.owner) - .finally(() => { - if (this.session?.id === session.id) { - this.session = null - } - }) + try { + await getSpeechSttService(this.requireStore()).stopDictation(session.owner) + } finally { + if (this.session?.id === session.id) { + this.session = null + } + } } private requireStore(): RuntimeStore { @@ -227,3 +212,7 @@ export class RuntimeMobileDictationController { return store } } + +function transcriptOf(session: MobileDictationSession): string { + return [...session.finalTexts, session.partialText].join(' ').trim() +} diff --git a/src/main/runtime/runtime-rpc-client-attach-placement.test.ts b/src/main/runtime/runtime-rpc-client-attach-placement.test.ts index 9cb9bfc34e7..0a33d34260f 100644 --- a/src/main/runtime/runtime-rpc-client-attach-placement.test.ts +++ b/src/main/runtime/runtime-rpc-client-attach-placement.test.ts @@ -19,7 +19,6 @@ function runtimeRecordingPlacement() { machineNameReady: async () => undefined, getStatus: () => ({ graphStatus: 'unavailable' }), cleanupSubscriptionsForConnection: () => {}, - cancelMobileDictationForConnection: () => {}, onClientDisconnected: () => {}, placeUnshownLiveTerminals } as unknown as OrcaRuntimeService diff --git a/src/main/runtime/runtime-rpc/runtime-rpc-lifecycle.ts b/src/main/runtime/runtime-rpc/runtime-rpc-lifecycle.ts index 8ad9e2e15d3..6f526959ff3 100644 --- a/src/main/runtime/runtime-rpc/runtime-rpc-lifecycle.ts +++ b/src/main/runtime/runtime-rpc/runtime-rpc-lifecycle.ts @@ -269,7 +269,6 @@ export class RuntimeRpcLifecycle extends RuntimeRpcWebSocketDispatch { this.mobileDesktopRelay?.closePhoneConnection(socket.connectionId) // Stream cleanup is socket-scoped; disconnect state spans all transports for that device. this.runtime.cleanupSubscriptionsForConnection(socket.connectionId) - this.runtime.cancelMobileDictationForConnection(socket.connectionId) // Optional: runtime stand-ins without staged saves must still close cleanly. void this.runtime.stagedFileSaves?.abortConnection(socket.connectionId) this.binaryMessageRouter.deleteConnection(socket.connectionId) diff --git a/src/main/runtime/runtime-service-command-surface.ts b/src/main/runtime/runtime-service-command-surface.ts index aab4c0ebec2..9f708f7b830 100644 --- a/src/main/runtime/runtime-service-command-surface.ts +++ b/src/main/runtime/runtime-service-command-surface.ts @@ -82,7 +82,6 @@ export type RuntimeServiceCommandSurface = { feedMobileDictation: RuntimeMobileDictationController['feed'] finishMobileDictation: RuntimeMobileDictationController['finish'] cancelMobileDictation: RuntimeMobileDictationController['cancel'] - cancelMobileDictationForConnection: RuntimeMobileDictationController['cancelForConnection'] getAllBrowserDrivers: RuntimeBrowserDriverController['getAll'] reclaimBrowserForDesktop: RuntimeBrowserDriverController['reclaimForDesktop'] notifyMessageArrived(handle: string, messageType?: string): void @@ -195,7 +194,6 @@ export function installRuntimeServiceCommandSurface( feedMobileDictation: dictation.feed.bind(dictation), finishMobileDictation: dictation.finish.bind(dictation), cancelMobileDictation: dictation.cancel.bind(dictation), - cancelMobileDictationForConnection: dictation.cancelForConnection.bind(dictation), getAllBrowserDrivers: browsers.getAll.bind(browsers), reclaimBrowserForDesktop: browsers.reclaimForDesktop.bind(browsers), waitForMessage: waiters.wait.bind(waiters), diff --git a/src/main/runtime/terminal/layout/terminal-client-disconnect.ts b/src/main/runtime/terminal/layout/terminal-client-disconnect.ts index 737e8262348..e0753045be2 100644 --- a/src/main/runtime/terminal/layout/terminal-client-disconnect.ts +++ b/src/main/runtime/terminal/layout/terminal-client-disconnect.ts @@ -15,7 +15,6 @@ export abstract class OrcaRuntimeWithOnClientDisconnected extends OrcaRuntimeWit onClientDisconnected(clientId: string): void { this.revokeTerminalFileGrantsForClient(clientId) - this.mobileDictation.cancelForClient(clientId) for (const [ptyId, entry] of this.pendingRestoreTimers) { if (entry.clientId === clientId) { diff --git a/src/shared/rpc-contract/speech-params.ts b/src/shared/rpc-contract/speech-params.ts index a6970b34c50..043585c42f1 100644 --- a/src/shared/rpc-contract/speech-params.ts +++ b/src/shared/rpc-contract/speech-params.ts @@ -9,6 +9,9 @@ export const PCM_BYTES_PER_SAMPLE = 2 export const MAX_DICTATION_AUDIO_SECONDS = 5 +// The host drops a phone dictation it hasn't heard from for this long, so the phone waits no longer for a reconnect. +export const MOBILE_DICTATION_RECONNECT_GRACE_MS = 30_000 + export const MAX_DICTATION_AUDIO_CHUNK_BYTES = DICTATION_SAMPLE_RATE * PCM_BYTES_PER_SAMPLE * MAX_DICTATION_AUDIO_SECONDS