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.
This commit is contained in:
Nathan Esquenazi committed 2026-07-10 20:17:33 -07:00
1 parent 18845fd000
commit 762fd01ae2
2 files changed
+31 -2

No files matched your search

+7
View File
@@ -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<event.results.length;i++){
@@ -114,7 +114,9 @@ const continuousAfterEnsure = recognition.continuous;
// Simulate _startMicCapture's speech branch entering an active session.
_activeCaptureMode = 'speech';
_speechStopRequested = !!input.stopRequested;
_micRestartCount = 0;
// #5294 regression: allow pre-seeding the restart budget so we can prove a real
// result resets it (a long productive dictation must not silently hit the cap).
_micRestartCount = (typeof input.preRestartCount === 'number') ? input.preRestartCount : 0;
window._micActive = true;
recognition.start(); // initial start (mimics _startMicCapture)
const startCallsAfterStart = recognition.startCalls;
@@ -131,6 +133,7 @@ const out = {
continuousAfterEnsure: continuousAfterEnsure,
restarted: recognition.startCalls > 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"
)