Skip to content

Phantom 'Voice input failed' alert when navigating back from a child/subagent session #197

Closed

Description

@omgoshjosh

Backing out of a subagent (child) session shows a "Voice input failed" alert on the parent session screen even though voice input was never used.

Root cause: on Android, expo-speech-recognition's abort() unconditionally emits a module-global {error: "aborted"} event — even when no recognition session is active (see ExpoSpeechRecognitionModule.kt, the Function("abort") handler sends the event before calling the service). useSpeech's unmount cleanup calls abort() (the comment "abort() is a no-op when not listening" is not true on Android), and because useSpeechRecognitionEvent listeners are module-global, the still-mounted parent session screen's error listener receives "aborted", sets error, and the alert fires.

Repro: open a session, navigate into a subagent session, press back → "Voice input failed" alert on the parent. No mic use required.

Fix:

  1. Treat "aborted" as benign in the error handler, same as "no-speech".
  2. In the unmount cleanup, only call abort() if this hook instance actually started listening.

🤖 Generated with Claude Code

Activity

  1. mikeboiko commented on Nov 11, 2026

    @mikeboiko

    For me, it's not phantom - I get this every time.

  2. dzianisv commented on Nov 13, 2026

    @dzianisv
    Owner

    Claim: Ember (owning lead) is taking this task — fix the phantom 'Voice input failed' alert on back-navigation from a subagent session (#197). Branch: fix/197-phantom-voice-alert. Scope: useSpeech / speech error handling only — treat aborted as benign and only call abort() when this hook instance actually started listening. No other behavior changes.

  3. dzianisv commented on Nov 13, 2026

    @dzianisv
    Owner

    Context pack — #197 phantom 'Voice input failed' alert

    Lead: Ember (owning lead). Claim: comment 6855120990. Branch: fix/197-phantom-voice-alert (merged, branch deletable). Work Log row: Notion — Done 2026-11-13. Stage: DONE (Stage 5 + 5.5). Design gap: comment 6855230505. Stage 5 verdict: comment on #197 (merge comment) — see below.

    What was asked: no 'Voice input failed' alert when backing out of a subagent session without ever using voice.

    Root-cause (verified against code at HEAD e844493): cleanup abort() at src/lib/speech.ts:83 → module-global {error: "aborted"} → parent listener fell through to setError at speech.ts:51 → alert at app/session/[id].tsx:153-159. Confirmed by mikeboiko (2026-11-11): 'I get this every time.'

    Acceptance criteria: (1) no phantom alert on back-navigation without voice use; (2) real voice errors still alert; (3) aborted is benign, like no-speech; (4) unmount cleanup calls abort() only if this hook instance actually started listening; (5) no changes outside useSpeech / speech error handling.

    Sweep: only 2 speech abort() sites; useSpeechRecognitionEvent only in speech.ts; only error consumer [id].tsx:157-159; only useSpeech consumer [id].tsx:147. No second instance of this defect class exists.

    Stage 3 verdict: PR #235, head a8b376e — new src/lib/speech-errors.ts + speech.ts edits (port of #182's speech hunks; no test file, no other #182 fixes). Gate: check:versions aligned, typecheck clean, suite 380 pass / 0 fail.

    Stage 4 verdict: PASS, comment 6855347929 (head a8b376e named, test-count delta 0, gates re-run by reviewer: typecheck clean, 380/380). Note: claude-code-review/SKILL.md does not exist on this VM — reviewer followed the method directly.

    Stage 5 verdict: MERGED as 38316ae5 (parents 2667ad5c + c657b225; files exactly the 2 speech files; verified by API readback). Main moved while CI ran (keyboard PR #236 merged as 2667ad5c), so the branch was rebased onto 2667ad5c → new head c657b225; rebase preserved the reviewed tree exactly (diff vs main still 2 files, +28/−4); local gate re-run on c657b225 (395 pass / 0 fail — the extra 15 tests came from #236). CI green on c657b225 before merge. Issue #197 auto-closed via Fixes #197 (verified: state closed).

    Stage 5.5 outcome: checked — no update needed. No speech design doc exists in Notion; the fix changes nothing the PRD promises. Recorded on #197.

    Open residual: on-device verification of the acceptance criteria (no phantom alert on back-nav; real errors still alert) remains for manual/phone verification on the next build. No release/tag from this task — release is the product owner's separate act.

    Decisions: simple bug lane; port (not fresh rewrite) of #182's speech hunks.

  4. dzianisv commented on Nov 13, 2026

    @dzianisv
    Owner

    Design gap — why this shipped wrong. There was no design or contract for voice input to check this against: no speech/voice design doc exists in Notion (PRD §7 lists voice input beyond OS dictation as a non-goal; the TDD lists speech.ts as a one-line file entry only), so the hook was written from the library's happy path with no stated invariants. Three specific gaps compounded:

    1. A false assumption was written into the code as fact. speech.ts commented the unmount cleanup with "abort() is a no-op when not listening" — untrue on Android, where abort() emits a module-global {error: "aborted"} event even with no recognition active. Nobody verified the comment against the native module.
    2. The error handler had no benign-error classification. Only no-speech was carved out; every other error string fell through to setError, including aborted — the documented, expected outcome of calling abort(). There was no allowlist of "not actually a failure" codes and no single place owning that decision.
    3. The hook had no notion of ownership. Nothing tracked whether this hook instance had started listening, so cleanup could not distinguish "stop my session" from "shout into a shared channel" — and the alert layer ([id].tsx) fires on any non-null error with no filtering of its own.

    Going forward: benign speech errors are classified in exactly one place (src/lib/speech-errors.ts), and unmount cleanup only aborts when this hook instance actually started listening.

    Investigation verdict (code at HEAD e844493): the JS-side chain is fully verified — cleanup abort() at src/lib/speech.ts:83, fatal fall-through at speech.ts:51, alert at app/session/[id].tsx:153-159. The Kotlin-level claim (abort emits even with no session active) is reported but not independently verified in this checkout (no node_modules); the fix is correct regardless, since aborted is the documented expected outcome of calling abort(). Sweep: abort() callers repo-wide — only the 2 speech sites (speech.ts:74 cancel, :83 cleanup); useSpeechRecognitionEvent exists only in speech.ts; the only error consumer is [id].tsx:157-159; the only useSpeech consumer is [id].tsx:147. No second instance of this defect class exists.

  5. added a commit that references this issue on Nov 13, 2026
  6. dzianisv commented on Nov 13, 2026

    @dzianisv
    Owner

    Stage 5 — merged. PR #235 merged as 38316ae5 (parents 2667ad5c + c657b225, files: src/lib/speech-errors.ts, src/lib/speech.ts — verified by API readback). Stage 4 review PASS (comment 6855347929, reviewed tree a8b376e); the branch was rebased onto main 2667ad5c (keyboard PR #236 landed first) — the rebase preserved the reviewed tree exactly (diff vs main still 2 files, +28/−4) and the local gate was re-run on the rebased head c657b225 (check:versions aligned, typecheck clean, full suite 395 pass / 0 fail). CI green on c657b225 before merge. This issue auto-closed via Fixes #197.

    Stage 5.5 — Design/PRD sync: checked, no update needed. No speech/voice design doc exists in Notion to update, and the fix changes nothing the PRD promises (voice input beyond OS dictation remains a §7 non-goal; the fix only stops a false alert). No Notion edit made.

    Residual: on-device verification of the acceptance criteria (no phantom alert on back-navigation from a subagent session; real voice errors still alert) remains for manual/phone verification on the next build — no e2e harness for the native speech module exists in the repo. No release/tag from this task — release is the product owner's separate act.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions