You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
isolate Desktop continuous-voice work by capture/turn lifetime so stale async completion cannot submit, stop, or re-arm a superseded turn
keep capture active while the assistant is busy, then use sustained-speech barge-in with adaptive ambient thresholds instead of a fixed instantaneous level
make mute, stop, unmount, supersession, playback completion, and React StrictMode replay deterministically invalidate or restore the right lifetime
This is the current-main Desktop safety slice of #94462. It ports the relevant behavior from the zer0-voice donor architecture into Hermes' existing Desktop hooks; it does not import donor code, add a runtime dependency, or introduce a second voice control plane. #77111 is architecture context only.
Problem
The existing Desktop path could let late getUserMedia, transcription, playback, or busy-state completion act on a turn that had already been muted, stopped, unmounted, or superseded. Listening also stopped during generation/playback, preventing natural barge-in, while a fixed input threshold was brittle across microphones and rooms.
Approach
The change stays within seven Desktop files:
monotonic capture and turn lifetimes reject stale asynchronous work
cancellation checks cover every post-await boundary and invalidate pending submission/re-arm work
recording continues through the busy phase so sustained speech can interrupt playback and the active request
a bounded ambient noise floor produces adaptive start/continue thresholds
monitor setup/teardown is idempotent and React StrictMode effect replay restores mounted ownership without allowing real-unmount resurrection
Hermes remains authoritative for prompt submission, cancellation, durable history, tools, approvals, and actions. The acoustic monitor consumes local amplitude samples only; it adds no network path, transcript sink, credential path, or server-side tool authority.
Current-main overlap and scope
This branch is based on current upstream main at f751a8c5467c41500e505d90cb0eb8b70929080f. A fresh issue/PR sweep found adjacent voice work (including provider transcription, silence-duration, playback, and re-arm changes), but no open PR owning this exact lifetime + adaptive-barge behavior. The diff is Desktop-only: 7 files, 461 insertions, 30 deletions.
Verification
The regression suite was exercised RED/GREEN, including sabotage runs that restored the old lifetime behavior and confirmed the new tests fail before passing with the fix.
barge-in, stop, monitor idempotency, playback re-arm, and adaptive acoustics
Desktop typecheck passed
targeted ESLint with --max-warnings=0 passed
targeted Prettier check passed
git diff --check passed
The repository's Python wrapper tests could not run in the review worktree because no pytest-capable virtualenv was available; those tests do not execute the changed Desktop TypeScript paths.
Risk and rollback
The main risk is browser media timing across effect replay and teardown. Tests cover stale completion, StrictMode replay, and each cancellation boundary. Rollback is the two commits in this PR; there is no schema, migration, config, or backend dependency.
Attribution and disclosure
Behavior and architecture were selectively reimplemented from kvnloo/zer0-voice, authored by Kevin Loo (kvnloo), behind Hermes-native Desktop contracts. No zer0-voice source or runtime dependency was copied into this repository.
This contribution was developed and reviewed with AI assistance from Hermes Agent; Kevin Loo directed the work, supplied the donor architecture, and is the human contributor/submitter.
AI code review — automated review for reference, author can ignore or act on any point.
This PR implements rigorous lifetime isolation for voice turns in the Desktop app, addressing a class of bugs where stale async resolutions (from getUserMedia, STT, or settle waits) could mutate shared state after the user ended, muted, or superseded a turn. The lifetimeRef counter pattern — incremented on every turn-invalidating action and checked at every await boundary — is the correct React pattern for this. The test coverage is exceptional: the parametrized ['end', 'mute', 'supersede', 'unmount'] cancellation matrix and the late-STT-rejection test pin the exact invariants the implementation protects.
A few observations:
AdaptiveAcousticThreshold EMA adaptation rate (voice-acoustics.ts:25): The noise floor uses floor * 0.92 + level * 0.08 — an 8% weight EMA that adapts slowly. With 40 quiet samples (as in the test), the floor rises gradually. In practice at 60fps rAF cadence, 40 samples ≈ 0.67s of quiet observation. If a user speaks briefly then pauses, the floor could still be adapting from the pre-speech baseline. Consider whether the adaptation should pause during speech (the observeQuiet guard if (level >= this.startThreshold) return handles this, but the caller must also avoid calling it during speech — the use-mic-recorder.ts code does this correctly with if (!heardSpeechRef.current && normalized < speechThreshold)).
Separate start/end thresholds (voice-acoustics.ts:17-22): startThreshold = floor + 0.045 and endThreshold = floor + 0.02. The lower end threshold prevents premature silence cutoffs during intra-word pauses. The use-mic-recorder.ts change to check normalized < endThreshold (instead of normalized < speechThreshold) for silence detection is the right application of this. However, if ambient noise rises between the start and end of an utterance, the end threshold could be lower than the current ambient level, making silence detection impossible. The EMA's slow adaptation mitigates this, but it's worth noting.
Unmount useEffect with handle dependency suppression (use-voice-conversation.ts:802-817): The eslint-disable react-hooks/exhaustive-deps for handle is justified — handle is recreated every render, so depending on it would tear down the active turn. The comment explains this well. However, the cleanup calls handle.cancel() which captures the handle from the first render (when the effect ran). If handle.cancel() behavior changed between renders, the unmount cleanup would use the stale handle. In practice, cancel is a stable identity from useMicRecorder's useCallback, so this is safe — but worth a comment noting that handle.cancel must remain stable across renders.
The voice-barge-in.ts refactor to use AdaptiveAcousticThreshold instead of manual floor sampling (with the median-of-sorted-samples approach) is cleaner and shares the same adaptation logic between capture and barge-in paths.
Capture/turn lifetime isolation + sustained-speech barge-in + adaptive ambient thresholds is the right Desktop safety slice: stale async completion must not submit/stop/re-arm a superseded turn.
Still draft; GitHub reports MERGEABLE but mergeStateStatus: BLOCKED and empty live status rollup — rebase onto current main, undraft, re-run focused Desktop Vitest (mic-recorder / voice-conversation / voice-acoustics / barge-in) + typecheck before merge desk.
Close competing Desktop turn-lifetime / adaptive-barge duplicates only after this lands and they are true duplicates; otherwise rebase them onto this tip.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
comp/desktopElectron desktop app (apps/desktop/*)P3Low — cosmetic, nice to havetool/ttsText-to-speech and transcriptiontype/bugSomething isn't working
3 participants
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This is the current-
mainDesktop safety slice of #94462. It ports the relevant behavior from thezer0-voicedonor architecture into Hermes' existing Desktop hooks; it does not import donor code, add a runtime dependency, or introduce a second voice control plane. #77111 is architecture context only.Problem
The existing Desktop path could let late
getUserMedia, transcription, playback, or busy-state completion act on a turn that had already been muted, stopped, unmounted, or superseded. Listening also stopped during generation/playback, preventing natural barge-in, while a fixed input threshold was brittle across microphones and rooms.Approach
The change stays within seven Desktop files:
Hermes remains authoritative for prompt submission, cancellation, durable history, tools, approvals, and actions. The acoustic monitor consumes local amplitude samples only; it adds no network path, transcript sink, credential path, or server-side tool authority.
Current-main overlap and scope
This branch is based on current upstream
mainatf751a8c5467c41500e505d90cb0eb8b70929080f. A fresh issue/PR sweep found adjacent voice work (including provider transcription, silence-duration, playback, and re-arm changes), but no open PR owning this exact lifetime + adaptive-barge behavior. The diff is Desktop-only: 7 files, 461 insertions, 30 deletions.Verification
The regression suite was exercised RED/GREEN, including sabotage runs that restored the old lifetime behavior and confirmed the new tests fail before passing with the fix.
getUserMediacleanup--max-warnings=0passedgit diff --checkpassedThe repository's Python wrapper tests could not run in the review worktree because no pytest-capable virtualenv was available; those tests do not execute the changed Desktop TypeScript paths.
Risk and rollback
The main risk is browser media timing across effect replay and teardown. Tests cover stale completion, StrictMode replay, and each cancellation boundary. Rollback is the two commits in this PR; there is no schema, migration, config, or backend dependency.
Attribution and disclosure
Behavior and architecture were selectively reimplemented from
kvnloo/zer0-voice, authored by Kevin Loo (kvnloo), behind Hermes-native Desktop contracts. Nozer0-voicesource or runtime dependency was copied into this repository.This contribution was developed and reviewed with AI assistance from Hermes Agent; Kevin Loo directed the work, supplied the donor architecture, and is the human contributor/submitter.
Part of #94462.