Skip to content

fix(orcad): voice dictation on headless orca serve hosts - #31217

Merged
nwparker merged 13 commits into
mainfrom
nwparker/orcad-speech
Oct 21, 2026
Merged

nwparker merged 13 commits into
mainfrom
nwparker/orcad-speech

Conversation

@nwparker

@nwparker nwparker commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 21 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​435 $\color{#cf222e}{\Huge{\mathbf{−}}}$​46 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​389
Prod 27 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​390 $\color{#cf222e}{\Huge{\mathbf{−}}}$​115 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​275

ELI5

Voice dictation from Orca Mobile stopped working on headless orca serve hosts once serve moved to the separate Node server (orcad). That server never switched on speech, so every voice request answered "this host has no speech services". With this PR the server handles dictation again. It uses the speech recognizer the installed Orca app already ships, and it keeps downloaded voice models in the same place as before.

What Changed

Before: on an orca serve host (Linux .deb, 1.4.229 and later), the mobile Voice screen shows speech_unavailable: this host has no speech services.

After:

  • orca serve from an installed app on Linux or macOS: the mobile Voice screen lists models again. Models download on the server, and dictation works with both on-device and cloud models.
  • orca serve on Windows: cloud models only. A loaded native module locks its files for as long as the server runs, which would make the next app update fail.
  • A server that has no on-device recognizer (for example a managed SSH or WSL server, which doesn't carry the ~70 MB recognizer): the screen lists only the cloud models. A cloud model shows as ready only when this server can actually decrypt the saved OpenAI key; a key the desktop sealed with Electron's secure storage doesn't count. A phone that had an on-device model selected is sent to the setup sheet instead of hitting an error.
  • A new phone paired with an older server: both the setup sheet and a mic tap say "Update Orca on this server to use voice dictation." instead of the raw error code.

How it works:

  • Downloads without Electron: the speech model manager and its download code imported Electron's app and net, so the Node server couldn't build them. The download now asks its host to open a request.
    • The desktop keeps using Chromium's network stack (host/electron-speech-services.ts).
    • orcad uses Node's https (speech/node-speech-download-request.ts). That adapter reports a redirect the same way Electron does, and keeps listening for errors after an abort so a late socket error can't crash the server.
    • The retry check (network/transient-download-error.ts) now also recognizes Node's error codes (ECONNRESET, ETIMEDOUT and so on), so an interrupted download on orcad resumes instead of starting over.
  • Speech switched on in orcad: orcad installs speech services at startup (orcad/orcad-speech-services.ts), and the dictation worker (stt-worker.js) now ships in the server package.
  • The recognizer: on Linux and macOS, orca serve finds the recognizer bundled with the installed app (resources/node_modules/sherpa-onnx-<platform>-<arch>) and passes its folder to orcad as ORCA_SPEECH_RECOGNIZER_DIR. The recognizer is a Node-API module, so the server's own Node build loads it as-is. Without that folder, getSherpaModulePath() returns null, the setup screen hides on-device models, and starting dictation answers voice_model_not_ready:unavailable.
  • The bug class: the parity check added in fix(orcad): mobile home stats on orca serve hosts, and a parity test that sees every way a host wires a service #31197 now requires both the desktop and orcad to install speech, so this can't silently go missing again.

Why

Run speech on the server, not advertise it as unsupported. The reporter's dictation worked on 1.4.205. Their machine already has the recognizer, and the server already has their models in its profile. Calling dictation unsupported would have taken away a feature that used to work.

Borrow the app's recognizer instead of shipping one in the server package. The recognizer is about 70 MB per platform. Managed SSH and WSL servers download the server package over the network, so adding it there would grow every one of those installs. orca serve already hands orcad the app's browser client the same way, so this follows an existing pattern. Those managed servers still get cloud dictation.

Hide what can't run, rather than adding a new model status. Older phones only know a fixed set of model statuses, and an unknown one would show up as a model they can pick. Leaving the model out of the list, and reusing the voice_model_not_ready: error that every phone build already sends to the setup sheet, works with both older and newer phones and needs no protocol change.

One shared download, with the host choosing the network stack. The download keeps its resume, retry and validation logic in one place. A separate Node-only downloader would have duplicated about 300 lines of tested code.

Linked Issue

Fixes #31174

Thanks to @delorean2020 for the report and for pointing at the missing service provider and the shipped recognizer module.

Builds on #31197 (merged), which extended the parity check this PR relies on.

Visual Proof

N/A. The server now answers these requests; no screen layout changed. The new mobile message is only a different error string. The live server checks are listed below.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

Verified live on macOS arm64, using a built orcad package (pnpm build:orcad) with its own HOME and user-data folder under /tmp.

With the app's recognizer folder passed in:

  • speech.models.list returned the full catalog. On main, the same call answers speech_unavailable.
  • speech.models.download whisper-tiny downloaded about 150 MB over Node https, through the release redirects, and the model reported ready.
  • The packaged out/orcad/stt-worker.js, running on plain Node with that recognizer, loaded the downloaded model. It answered ready, then final, then stopped on a second of audio.

Without the recognizer folder:

  • speech.models.list listed only the two OpenAI models.
  • Downloading whisper-tiny was refused with voice_model_not_downloadable.

Covered by tests only:

  • A full mobile dictation session against orcad. Start needs a real phone client.
  • orca serve finding the recognizer inside a real .deb install. Covered with temporary folder layouts.
  • Windows and Linux.

Unit tests added:

  • stt-worker-paths.test.ts: how orcad resolves the recognizer and the worker.
  • node-speech-download-request.test.ts: redirects, responses, and a late error after an abort.
  • orcad-speech-services.test.ts: the model list, downloads and dictation start on a server with no recognizer.
  • transient-download-error.test.ts: Node errors that should and shouldn't be retried.
  • Recognizer lookup in serve-orcad-launch.test.ts.
  • The mobile speech_unavailable message.

Gates run locally: the full static job (lint, format, anti-slop, code quality, pnpm tc, ratchets, generated catalogs), pnpm --filter mobile dictation tests, and mobile tsc.

Review

Codex (gpt-6.1-sol, high)

  1. Round 1: Node's dropped-connection errors weren't retried, so an interrupted download was deleted instead of resumed. Fixed in the shared retry check.
  2. Round 2: a desktop-sealed OpenAI key showed as "ready" on orcad. orcad now proves the key decrypts.
  3. Rounds 3 and 4: problems in the Windows recognizer copy (pruning, linked packages, partial copies).
  4. Round 5: in plaintext secret mode, another store's ciphertext was returned as if it were the key. The key read now refuses it.
  5. Final round on this head: see the follow-up comment.

Independent Opus review:

  1. Round 1 asked for the Windows file-lock handling.

  2. Round 2 was clean.

  3. The final pass found nothing blocking. I adopted three of its suggestions:

    • map an old server's refusal of a mic tap to the update message
    • always set the recognizer variable, so a value inherited from an Orca terminal can't point at another install
    • retry ENOTFOUND

    It also noted that a cloud key saved on the desktop can't be read by orcad (see the follow-ups).

Design check (ref-oss, after round 3; decided after round 5): I compared this with putting a "speech unsupported on headless hosts" capability in front of the mic. I kept speech running in orcad:

  • It restores what worked on 1.4.205, and the core path (download, worker, recognizer) is verified live.
  • The capability route needs a new capability plus mobile changes, and older phones would still show the mic.

Every late finding came from one side mechanism: copying the recognizer on Windows so the app's files stay unlocked. Comparable servers keep versioned copies that only their owning process cleans up. Rather than build that here, I removed the copy entirely. Windows orcad gets cloud dictation, and the launcher now only looks up a path.

Follow-ups:

  • On-device dictation on Windows orcad, through a copy the server itself owns once it holds the profile lock.
  • A way to set the OpenAI speech key on a headless host.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH and WSL managed servers: cloud models only. On-device models are hidden rather than failing.
  • Folder workspaces: not affected.
  • Wire compatibility: no new fields or message types. Older phones get a list they already understand and an error code they already handle.
  • Proxies: Node's https ignores proxy settings, so a server behind a proxy can't download models. The desktop path is unchanged.
  • Electron desktop: behaves the same, except that a dev checkout with no recognizer now reports it as unavailable instead of throwing on require.resolve.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@nwparker

Copy link
Copy Markdown
Contributor Author

Review status for head 5f7f8a5

  • Opus (final pass): nothing blocking. I adopted three of its suggestions in this head:

    • a mic tap on an old server now shows the update message
    • orca serve always sets the recognizer variable
    • ENOTFOUND is now retried
  • Codex (final round): two P2s, both about hosts that have no on-device recognizer (managed SSH and WSL servers, and Windows orca serve). I'm leaving both as follow-ups rather than widening this PR, because neither is caused by this change:

    1. No headless way to set the OpenAI speech key. That is the case on every host today: the key can only be saved from desktop Settings. This PR stops orcad from showing a key it can't read as "ready".
    2. Mobile can't select a cloud model. The setup sheet shows OpenAI rows as information only, on every host. MobileDictationSetupSheet.tsx is unchanged here.

    Before this PR, those hosts answered speech_unavailable for everything. Now they list the cloud models honestly, not ready, and on-device dictation works wherever the app's recognizer is present. That covers the reported Linux .deb case.

  • Full static job: passed locally on this head. Mobile dictation and session tests pass, and mobile tsc passes.

Follow-ups to file:

  • headless OpenAI speech key provisioning (RPC or CLI)
  • cloud-model selection in the mobile setup sheet
  • on-device dictation on Windows orcad through a copy the server itself owns

@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f4929d07-b65c-4f93-8bc4-7d382e33933c
📥 Commits

Reviewing files that changed from the base of the PR and between b4368e6 and 92e3c97.

📒 Files selected for processing: 1
  • mobile/src/host-screen/host-screen-reply-schema.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

📝 Walkthrough

The change adds Orcad speech-service wiring, local recognizer discovery, host-specific speech download transports, and STT worker packaging. Orcad receives the application recognizer path through its environment. Mobile dictation maps speech_unavailable failures to server-update guidance. Host platform values are normalized before schema output. POSIX install probing now checks required artifacts relative to the target directory.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🔵 Low · up to 92e3c

A narrow test-environment issue remains, but it does not affect runtime speech behavior and has a straightforward workaround.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 34 functions across 45 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary change: restoring voice dictation for headless Orca serve hosts.
Description check Passed The description is complete and follows the required template. It explains the user impact, implementation, rationale, linked issue, testing, platform coverage, follow-ups, and checklist status.
Linked Issues check Passed Issue [#31174] requires mobile voice dictation on headless Linux orca serve hosts. The PR wires speech services into orcad, packages stt-worker.js, discovers the installed Sherpa recognizer, and…
Out of Scope Changes check Passed The changes support [#31174]. Node download adapters, retry handling, recognizer discovery, worker packaging, key-readability checks, platform handling, install-completeness checks, and mobile error m…
Full details: Linked Issues check

Explanation

Issue [#31174] requires mobile voice dictation on headless Linux orca serve hosts. The PR wires speech services into orcad, packages stt-worker.js, discovers the installed Sherpa recognizer, and passes its directory to orcad. The PR adds model listing, downloads, dictation startup, compatibility handling, and automated tests for these paths.

Full details: Out of Scope Changes check

Explanation

The changes support [#31174]. Node download adapters, retry handling, recognizer discovery, worker packaging, key-readability checks, platform handling, install-completeness checks, and mobile error mapping enable or protect server-side dictation. Windows, macOS, cloud-only, and no-recognizer behavior remain within the same speech feature scope. No unrelated change is demonstrated.

✨ Finishing Touches: 💡 2
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bb7c302c-33da-494c-90e4-c5d2901145e3
📥 Commits

Reviewing files that changed from the base of the PR and between 8a4ef49 and 5f7f8a5.

📒 Files selected for processing: 38
  • config/scripts/orcad-entry-build.mjs
  • mobile/src/dictation/mobile-dictation-setup.test.ts
  • mobile/src/dictation/mobile-dictation-setup.ts
  • mobile/src/session/use-mobile-session-native-chat-dictation.ts
  • src/cli/runtime/launch-serve-runtime.test.ts
  • src/cli/runtime/launch.ts
  • src/cli/runtime/serve-orcad-launch.ts
  • src/cli/runtime/serve-speech-recognizer.test.ts
  • src/cli/runtime/serve-speech-recognizer.ts
  • src/main/host/electron-speech-services.ts
  • src/main/ipc/speech-worker-exit-lifetime.test.ts
  • src/main/network/transient-download-error.test.ts
  • src/main/network/transient-download-error.ts
  • src/main/orcad/orcad-host-adapters.ts
  • src/main/orcad/orcad-speech-services.test.ts
  • src/main/orcad/orcad-speech-services.ts
  • src/main/runtime/construction/runtime-composition-parity.test.ts
  • src/main/runtime/runtime-mobile-dictation-controller.ts
  • src/main/runtime/runtime-mobile-speech-catalog.ts
  • src/main/speech/model-manager-download-error.test.ts
  • src/main/speech/model-manager-download-resume.test.ts
  • src/main/speech/model-manager-progress-callback.test.ts
  • src/main/speech/model-manager-stream-cleanup.test.ts
  • src/main/speech/model-manager-windows-path.test.ts
  • src/main/speech/model-manager.test.ts
  • src/main/speech/model-manager.ts
  • src/main/speech/node-speech-download-request.test.ts
  • src/main/speech/node-speech-download-request.ts
  • src/main/speech/openai-api-key-store.test.ts
  • src/main/speech/openai-api-key-store.ts
  • src/main/speech/speech-model-download-response.ts
  • src/main/speech/speech-model-http-download.ts
  • src/main/speech/speech-runtime-service.ts
  • src/main/speech/stt-session-start.ts
  • src/main/speech/stt-worker-paths.test.ts
  • src/main/speech/stt-worker-paths.ts
  • src/shared/orcad-artifacts.ts
  • src/shared/speech-recognizer-package.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Comment on lines +9 to +23
const roots: string[] = []
afterEach(() => {
delete process.env[SPEECH_RECOGNIZER_DIR_ENV]
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})

// orcad: a packaged host with no Electron resources tree.
describe('speech worker paths on orcad (#31174)', () => {
it('has no on-device recognizer unless orca serve names one', () => {
installFakeAppEnvironment({ isPackaged: () => true })
expect(getSherpaModulePath()).toBeNull()
expect(hasLocalSpeechRecognizer()).toBe(false)
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear ORCA_SPEECH_RECOGNIZER_DIR before each test.

The suite deletes SPEECH_RECOGNIZER_DIR_ENV only in afterEach. The first test therefore uses the value it inherits from the parent process. serveWithOrcad always sets ORCA_SPEECH_RECOGNIZER_DIR on the orcad process. The comment in src/cli/runtime/serve-orcad-launch.ts says an Orca terminal can pass this value on to child processes. A developer can run vitest in a shell that orcad hosts. In that shell, the variable points to the installed app's real recognizer, which contains sherpa-onnx.node. getSherpaModulePath() then returns that directory, and the assertions on Line 21 and Line 22 fail.

💚 Proposed fix
-import { afterEach, describe, expect, it } from 'vitest'
+import { afterEach, beforeEach, describe, expect, it } from 'vitest'
 ...
 const roots: string[] = []
+beforeEach(() => {
+  delete process.env[SPEECH_RECOGNIZER_DIR_ENV]
+})
 afterEach(() => {

Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.

The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.

The parity test now requires both roots to install speech.

Reported-by: delorean2020 (@delorean2020)
…s, review fixes

- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
  interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
  in the profile, so a loaded addon never locks the app's files against
  an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
  helper per test file instead of repeated casts; comments.
…pt; prune superseded recognizer copies

Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).
…opy linked packages as real files

Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.
…loud dictation only

Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.
…s set the recognizer env; retry ENOTFOUND (final Opus review)
CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.
… of which zod copy resolves

With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.
…nt; narrow in a second step

Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.
…platform guard now zod is aligned

launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.
@nwparker
nwparker force-pushed the nwparker/orcad-speech branch from 4d67328 to f32b1da Compare October 21, 2026 02:27
@nwparker
nwparker merged commit fece03b into main Oct 21, 2026
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mobile voice dictation broken on headless orca serve host after 1.4.205 → 1.4.232 (speech_unavailable)

1 participant