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" + )