From 762fd01ae28a83a4f9dcc44ec3848abf04555901 Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Fri, 10 Jul 2026 20:17:33 -0700 Subject: [PATCH] fix(review): reset mic restart budget on a productive dictation result (#5294) _micRestartCount bounds the continuity auto-restart loop (_micMaxRestarts=20) so a stolen audio session can't spin tight. But it only reset on fresh-start / terminal-end / error -- never on a real onresult. So a long mobile dictation with 20 natural pauses (each producing speech) hit the cap mid-session and the mic silently stopped, undercutting the feature's whole purpose ("keep dictation active through pauses"). Reset _micRestartCount=0 in sr.onresult: a real result proves the restarts are PRODUCTIVE, not a failure spin. The cap still guards the failure case -- consecutive restarts that yield no speech (onend with no intervening onresult) keep incrementing and trip the bound. Regression test seeds the count AT the cap, fires a productive onresult, and asserts the ensuing pause still restarts (count reset to 0 -> 1). Verified fail-without / pass-with against boot.js. --- static/boot.js | 7 +++++ ...t_issue5294_mobile_dictation_continuity.py | 26 +++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/static/boot.js b/static/boot.js index 0314bc0c..02bbdc8c 100644 --- a/static/boot.js +++ b/static/boot.js @@ -919,6 +919,13 @@ function _micToastKeyForRecognitionError(error){ sr.onstart=()=>{ _finalText=''; }; sr.onresult=(event)=>{ + // #5294: a real result means the continuity restarts are PRODUCTIVE, not a + // stolen-audio-session tight loop — reset the restart budget so a long + // dictation with many natural pauses isn't silently capped at + // _micMaxRestarts. The cap still guards the failure case: consecutive + // restarts that yield no speech (onend without an intervening onresult) + // keep incrementing and trip the bound. + _micRestartCount=0; let interim=''; let final=_finalText; for(let i=event.resultIndex;i startCallsAfterStart, micActiveAfterPause: !!window._micActive, + restartCountAfter: _micRestartCount, taValue: ta.value, toasts: toasts, }; @@ -145,8 +148,10 @@ def driver_path(tmp_path_factory): return str(p) -def _run(driver_path, *, coarse=False, store=None, stop_requested=False): +def _run(driver_path, *, coarse=False, store=None, stop_requested=False, pre_restart_count=None): payload = {"coarse": coarse, "store": store or {}, "stopRequested": stop_requested} + if pre_restart_count is not None: + payload["preRestartCount"] = pre_restart_count result = subprocess.run( [NODE, driver_path, str(BOOT_JS_PATH), json.dumps(payload)], capture_output=True, text=True, timeout=30, @@ -201,3 +206,20 @@ class TestMobileKeepsSessionAlive: out = _run(driver_path, coarse=False, store={"hermes-mic-continuous": "true"}) assert out["continuousAfterEnsure"] is True assert out["restarted"] is True + + def test_productive_result_resets_restart_budget(self, driver_path): + # A real result means the continuity restarts are PRODUCTIVE, so the + # restart budget must reset — otherwise a long mobile dictation with many + # natural pauses silently hits _micMaxRestarts and dies mid-session. Seed + # the count AT the cap: onresult must reset it to 0 so the ensuing pause + # still restarts. Without the reset, count stays at the cap and the + # session would NOT restart. + out = _run(driver_path, coarse=True, pre_restart_count=20) + assert out["restartCountAfter"] == 1, ( + "a productive onresult must reset _micRestartCount (to 0), leaving 1 " + "after the subsequent natural-pause restart — not stay pinned at the cap" + ) + assert out["restarted"] is True, ( + "mobile dictation must keep restarting through pauses once a real " + "result has proven the session productive, even past the raw cap" + )