Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

First run: a fresh install reads as ready to set up, not as a failure; Dictate never hangs when session saving is off

Open
#379 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
22/100
Issue type
Bug
Clarity
Mostly clear
Activity status
Active
Tech stack
macos, swift

Research direction

The issue covers two separate bugs. Start with prepare(acquire:) in RecognitionProviders.swift and the missing-assets handling in RecognitionLocalModels.swift, then the savePendingCapture guard in AppModel.swift around line 766. Done means the new checks in RecognitionLifecycleChecks (--check-providers) and the case in scripts/test-capture-persistence.py pass, with the existing checks still green.

Written by the indexing model from the issue text.

Description

bug

Outcome: First run: a fresh install reads as ready to set up, not as a failure; Dictate never hangs when session saving is off.

Verified by an independent skeptic against the release train head d7cea35 (round four of the 15–16 October pass: energy, leaks, first run, keyboard, concurrency, settings propagation, public docs).

A fresh install reads as a failed speech setup: "Your saved work is available now", an orange triangle and Retry model before anything was downloaded

What a person sees: Every brand-new user is told that speech "needs a download… Your saved work is available now" when they have no saved work. Their first Snap & Talk session marks speech with an orange triangle and a Retry model link that cannot help, plus "If the model still can't be prepared…", which frames a model they never downloaded as a broken one. The designed first-run sentence, "Download Parakeet to turn speech into text on this Mac.", never appears on a real fresh Mac. Users who already have the model are unaffected.

Evidence (d7cea35): AppModel.swift:262 Task { await prepare() } → :273 engine.prepareCached() → RecognitionProviders.swift:103 prepare(acquire: false) → RecognitionLocalModels.swift:62-65 → load() throws .missingAssets with 'Parakeet needs a download before speech can be transcribed. Your saved work is available now.' (:113) when the files are absent. RecognitionProviders.swift:139-141 stores it as state.failure. RecognitionReadiness.swift:19-27: line returns failure.message before the .unavailable offer at :26. AppModel.swift:269 sets modelMessage = line and modelFailure = errorDescription. Surfaces: Home's engineBanner shows modelMessage (WorkbenchHome.swift:809) beside Download Parakeet (:824). The panel's Dictate row returns modelMessage + ' · Settings › Models' (WorkbenchControlTool.swift:384). Meetings prefixes it through MeetingModel.swift:153 → MeetingAdmission.swift:15. Snap & Talk gets engine: .init(name: model.modelMessage, ready: model.ready, failure: model.modelFailure, …) (WorkbenchHome.swift:186); needsAttention = failure != nil && !ready (ReadbackView.swift:83) draws the triangle and Retry model (:52-58) and the 'If the model still can't be prepared…' line (:63) instead of 'You can record for later…' (:66). Retry model reruns the same cached check. Coverage gap: RecognitionLifecycleChecks.swift:124 checks only the loader's error kind, and the gallery child opts out of cached preparation (docs/verification/2026-10-07-first-speech.md:36). Contract: workbench.md:233 'Idle missing assets are not described as preparing'.

Change: 1. The change. In RecognitionProviders.swift prepare(acquire:)'s catch: when !acquire and the error is a RecognitionFailure of kind .missingAssets, leave state.failure nil and admission .unavailable, call finishPreparation() and return without throwing. Not downloaded is the offer, not an error. The configuration check at :116, the acquire path (a download that yields no files stays a .missingAssets failure), and the invalidCache, load, storage and network results all stay as they are.

  • If the implementer prefers to keep throwing, ModelSettingsView operationFailure must also ignore .missingAssets. Otherwise switching from a local server to Parakeet shows a duplicate note.
  1. No Retry saved files flag, no hiding of Retry saved files, no change to workbench.md. requireAdmission's fallback (.missingAssets, "Open Models to prepare speech first. Your recording and saved work are kept.") still drives ReadbackModel.swift:1464's resume wait and the CLI --transcribe error.
  2. Checks in RecognitionLifecycleChecks (--check-providers).
    • Build an engine whose services.prepareCached calls RecognitionLocalModels.prepareCached(cache: <empty temp dir>). After prepareCached(), which must not throw, assert:
      • failure == nil
      • admission == .unavailable
      • !canTranscribe
      • line == "Download Parakeet to turn speech into text on this Mac."
      • transcribe() still throws a RecognitionFailure of kind .missingAssets
    • A cache with a corrupt vocabulary still records .invalidCache as a failure.
    • An acquire whose load finds no files still records .missingAssets as a failure.
  3. Optional check in ReadbackChecks. A NarrationEngine built from that snapshot (name: line, ready: false, failure: `

Acceptance: --check-providers (RecognitionLifecycleChecks)

  • An engine using the real RecognitionLocalModels.prepareCached on an empty temporary cache returns from prepareCached() without throwing. Afterwards:
    • snapshot.failure == nil
    • admission == .unavailable
    • !canTranscribe
    • line == "Download Parakeet to turn speech into text on this Mac."
    • transcribe(url) still throws a RecognitionFailure of kind .missingAssets
  • A cache with an invalid vocabulary still ends with failure.kind == .invalidCache.
  • An acquire whose load finds no files still ends with failure.kind == .missingAssets.
  • The existing stalled-download check still passes.

--check-readback

  • The existing 'kept take waits while speech prepares' checks (ReadbackChecks.swift:1112-1160) still pass.
  • A NarrationEngine built from the fresh snapshot is not needsAttention.

Manual source check

  • Switching from a local server to Parakeet on an empty cache shows no WorkbenchNote, only the offer line and Download Parakeet.

check-surfaces.py and the gallery

  • check-surfaces.py still passes.
  • If the gallery change is made, the first-speech offer render's text comes from an engi

Value: 3 of 5 · Grammar: Quality · Risk: Low, with one trap.

  • The trap. If the cached path keeps throwing, ModelSettingsView operationFailure turns the now failure-less .missingAssets into a duplicate note when switching to Parakeet without the model. Either don't throw for .missingAssets on the cached path, or filter it there.
  • Callers that stay correct.
    • Snap & Talk's resumed-take wait still matches .missingAssets through requireAdmission's fallback.
    • The CLI --transcribe still fails, now with "Open Model
Dictate stays on 'Tidying your words…' and a Shortcuts run never returns when session saving is disabled

What a person sees: This path is rare but deterministic. It begins after Workbench says 'Your saved session could not be opened or backed up. Automatic saving is disabled…'. The next dictation records, recognises and then stops at 'Tidying your words…' with no error, Retry or Cancel. Every later dictation, Retry and Shortcut is refused until relaunch. A 'Transcribe with Workbench' Shortcut keeps spinning, and cancelling it in Shortcuts does nothing.

Evidence (d7cea35): AppModel.swift:251-253: the backup-failure branch reports and returns before loaded = true (:256), skipping restoreCaptureRecovery (:258) and prepare() (:262). :767: guard loaded, let record = captureRecovery.pending, let capture = record.capture else { return false } returns without fail(), while every other refusal in savePendingCapture goes through fail() (:778). :763: commitRecognizedCapture returns savePendingCapture() ? capture.id : nil. :654-655: guard let captureID = try commitRecognizedCapture(...) else { return } leaves phase .cleaning (set at :641) and never calls shortcutRequest.finish. :626-628: the task's defer clears transcriptionTask, so cancelCurrentCapture's guard [.transcribing, .cleaning].contains(phase), transcriptionTask != nil (:552) returns before shortcutRequest.cancel(). The Shortcuts cancellation handler routes there (Shortcuts.swift:105; AppModel.swift:31-35). DictationRequest (Shortcuts.swift:69-80) holds the continuation from AppModel.swift:25-27 indefinitely. fail() (AppModel.swift:1248-1253) is the path that resets phase and finishes the Shortcut.

Change: AppModel.savePendingCapture (Sources/LocalVoice/AppModel.swift:766-767): split the guard. The missing-record case keeps returning false quietly. Add a new branch first:

guard loaded else { canRetry = false; fail("Automatic saving is off for this session, so this dictation was not added to History or sent to your app. The words are in Dictate. Copy them, then use Discard recovery… to dictate again, or reopen Workbench.", code: "dictate.saving_disabled"); captureFailure = self.error; onPhaseChange?(); return false }

This mirrors the existing save-failure branch at :776-779. fail() sets phase to .idle, raises the failure on Dictate and finishes any Shortcut exactly once (:1248-1253). The retry door at :604 now reports the same reason instead of doing nothing.

Out of scope, deliberately: moving prepare(), restoreCaptureRecovery or the early return in init, and any "deliver without History" behaviour. Those are behaviour decisions.

Add a case to scripts/test-capture-persistence.py, next to the "shortcut" case at lines 676-685.

Acceptance: Add a case to scripts/test-capture-persistence.py and run it via scripts/test.sh:57, or with PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-capture-persistence.py.

(a) Shortcut run: a CaptureHarness with loaded = false begins a shortcutRequest with a counting completion, then runs run(importedURL, owned: false) and await finish(model). Assert:

  • replies == 1 and the result is a failure
  • shortcutRequest.id == nil and phase == .idle
  • transcript equals the recognised text, history is empty and store.calls == 0
  • a later cancelShortcut(id) leaves replies at 1

(b) Owned Dictate run: makeRecording(wav) then run(url, owned: true) with loaded false. Assert:

  • phase == .idle and transcriptionTask == nil
  • captureFailure contains "Automatic saving is off"
  • canRetry == false, hasCaptureRecovery == true and canDiscardCaptureRecovery == true

The case must fail on d7cea35, where phase stays .cleaning and replies == 0; the harness's finish awaits the task, so the old code fails without hanging. It must pass after the change, with the rest of the script still green.

Value: 2 of 5 · Grammar: Quality · Risk: Low. The new branch runs only when loaded == false, which happens only after the session failed to load and could not be backed up (AppModel.swift:251-253). It reuses fail(), which already finishes Shortcuts once and resets the phase. If live preview text was already written, it keeps that text in the field, exactly as the existing save-failure branch does. The one behaviour change in the normal path is none. In the disabled-saving path, Retry saving is hidden and a plain sentence plus Discard r

Refs #15


Build-ready record from round four of the overnight lead pass, verified by an independent skeptic per item. Closes only after native acceptance.

Dominant language
Swift
Stars
0
Forks
1
Avg merge
8h 5m
Merged PRs (30d)
215

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from Ship-Work/workbench

All issues in Ship-Work/workbench

Similar issues

More Swift issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.