Skip to content

fix: recording stuck-stop watchdog, keychain persistence, error feedback, overlay stop path#55

Merged
Szowesgad merged 13 commits into
developfrom
fix/runtime-hardening
Jul 9, 2026
Merged

fix: recording stuck-stop watchdog, keychain persistence, error feedback, overlay stop path#55
Szowesgad merged 13 commits into
developfrom
fix/runtime-hardening

Conversation

@m-szymanska

Copy link
Copy Markdown
Contributor

PR type: bugfix (runtime hardening)

Problems addressed

All findings come from an adversarially-verified deep quality recon (42 findings total; the P1s below were CONFIRMED by independent verification):

  1. [P1] Hold-to-record path has no stuck-stop watchdog. finish_recording_locked awaited process_recording with no timeout. A hung STT final pass (e.g. Metal/Whisper stall) left the app permanently in Busy with hotkeys ignored. The toggle path already had STOP_TIMEOUT protection; the hold path did not.
  2. [P1→follow-through] Stuck-stop recovery leaked session fields. recover_from_stuck_stop bypassed the full session reset, leaking IS_ASSISTIVE_SESSION so the tray misreported an assistant session after recovery (both toggle and hold paths).
  3. [P2] Setting the documented CODESCRIBE_DATA_DIR silently disabled the Keychain. is_test_env() treated CODESCRIBE_DATA_DIR (and CI) as "test environment", so API keys and OAuth tokens never persisted across restarts for anyone using the documented data-dir override.
  4. [P2] Transcription failure was a silent error!() log. The user got no feedback at all when a recording failed to transcribe.
  5. [P3] Paste error dropped the AI-formatted transcript. The formatted text was persisted only after a successful paste; a transient paste failure lost the formatting layer.
  6. [P2] Overlay warmup-watchdog/error path left the composer mic dead. fireWarmupWatchdog/onError never ran the stop-side path, so chat.dictationBlocked stayed true until the next successful dictation session.
  7. [P2] Valid API keys reported "could not verify". Key-liveness classified HTTP 400 responses (e.g. model_not_found) as network errors, even though a 4xx response proves the key passed auth.

Fixes

  • app/controller/mod.rs: shared STOP_TIMEOUT const; hold path wrapped in tokio::time::timeout with the same recover_from_stuck_stop recovery; recovery now performs the full reset_finished_recording_state.
  • app/controller/mod.rs: transcription failure now emits the existing EngineEventWire::Warning (already wired end-to-end: tray Error state + on_error on the Swift side); formatted transcript is persisted before paste.
  • core/config/keychain.rs: CODESCRIBE_DATA_DIR and CI removed from is_test_env(); CODESCRIBE_DISABLE_KEYCHAIN remains the only user-facing kill switch. Test binaries are still auto-detected (cfg!(test) + cargo deps-path + RUST_TEST_THREADS), so the test suite never touches the real Keychain.
  • core/llm/key_liveness.rs: 401/403 → invalid key, 429/402 → quota, other 4xx → key verdict OK (server answered ⇒ auth passed), 5xx/transport → network. No new enum variant; bridge/Swift untouched.
  • macos/.../OverlayState.swift: minimal abortRecordingSession path used by warmup watchdog and onError — fires onRecordingStopped exactly once (guarded by finalized), so dictationBlocked resets after failures.
  • bridge/src/hotkeys.rs: test-only serialization of hotkey tests that share process-wide state (pre-existing race surfaced by the new watchdog test).

Test plan (commands actually run, all green at final HEAD)

cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
make app   # Swift debug build, BUILD SUCCEEDED

New tests: paused-time watchdog regression test (hangs process_recording, asserts Idle + cleared session fields), keychain is_test_env coverage, key-liveness classification cases (400/401/403), Warning-on-failure and save-before-paste invariants.

Smoke notes (manual, recommended before merge)

  1. Hold-record with a forced STT stall → app returns to Idle within STOP_TIMEOUT, hotkeys stay alive, tray shows no stale assistant session.
  2. CODESCRIBE_DATA_DIR=/tmp/csdata + save an API key in Settings → restart → key still present.
  3. Kill dictation during warmup (watchdog fires) → composer mic usable without an app restart.

Out of scope / backlog

Streaming-pipeline behaviorals (stale corrections, unbounded window_text, utterance joining), agent-turn cancellation not aborting spawned tools, multi-turn composer images, audio auto_silence defaults, thermal shedding order, FFI panic strategy — all recorded in the recon report as backlog; deliberately not touched here.

🤖 Generated with Claude Code

vetcoders-agents and others added 8 commits July 9, 2026 12:20
- Route stuck-stop recovery through reset_finished_recording_state.

- Clear assistive session metadata and transient mode fields consistently.

- Make toggle and hold recovery share the same full reset surface.
- Share STOP_TIMEOUT between toggle and hold stop paths.

- Wrap hold-path process_recording in tokio timeout and recover to Idle on expiry.

- Add a paused-time regression test that hangs process_recording and verifies session cleanup.

- Enable Tokio test-util for deterministic watchdog coverage.
- Mark CODESCRIBE_DATA_DIR bridge coverage as serial across the crate.

- Serialize optimistic-overlay compensation tests that install shared controller/listener state.

- Keep cargo test --workspace deterministic under default parallel test execution.
CODESCRIBE_DATA_DIR is a documented, production-valid override for the
data directory, but is_test_env() treated it (and CI) as a Keychain kill
switch. Setting it silently disabled the Keychain, so API keys and OAuth
tokens stopped persisting across restarts.

Drop CODESCRIBE_DATA_DIR and CI from is_test_env(); CODESCRIBE_DISABLE_KEYCHAIN
is now the only user-facing opt-out. Test binaries are still detected via the
existing harness signals (target/**/deps/ exe path, RUST_TEST_THREADS, cfg!(test)),
so the suite never touches the real Keychain. rust.yml runs no tests, so removing
CI has no effect on CI. Extract the pure policy into keychain_disabled_by_signals()
and pin the regression with unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Route warmup watchdog aborts through the recording stop-side callback.

- Route overlay listener errors through the same stop-side cleanup.

- Keep abort cleanup idempotent so normal finalize still fires onRecordingStopped once.
…key liveness

A 400 (e.g. model_not_found) means the server processed the request and
the key passed authentication, so the key is live. Previously such
responses fell through to the Network bucket and surfaced a misleading
"could not verify" message for a working key. Classify any non-quota,
non-auth client error (4xx) as Ok; keep 401/403 as Invalid, 429/402/quota
as NoQuota, and 5xx/transport failures as Network.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A failed processing/transcription result was only logged via error!(),
so the user got no signal. Broadcast an engine Warning over the existing
IPC channel in the Err branch of handle_processed_recording_result; the
bridge forwarder turns it into listener.on_error + a tray Error state on
the SwiftUI surface. Reuses the wired Warning variant — no new event
architecture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A paste failure early-returns from process_stopped_recording via `?`,
which previously skipped the final-transcript save that followed it —
losing the AI-formatted layer for that dictation. Move the needs_final_save
block ahead of the paste attempt so the formatted text is recoverable from
history even when delivery fails. History write is a disk-only side effect,
so the user-visible side-effect order (paste) is unchanged on happy path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several robustness improvements and bug fixes across the codebase. Key changes include surfacing transcription failures to the user via IPC warnings, implementing a watchdog timeout for hold-stop processing to recover to an Idle state if it stalls, and persisting the formatted transcript before attempting to paste to prevent data loss. Additionally, the Keychain-disable policy is refined so that setting CODESCRIBE_DATA_DIR or CI does not disable Keychain persistence, and non-auth 4xx client errors are now treated as successful key liveness checks. The macOS overlay state is also updated to properly abort recording sessions on errors. I have no additional feedback to provide as there are no review comments.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

vetcoders-agents and others added 4 commits July 9, 2026 14:40
Apply clippy::question_mark on stable 1.97 in parse path split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace manual Option::filter implementation in non_empty_transcript
with Option::filter, per clippy::manual_filter on stable 1.97.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Szowesgad Szowesgad merged commit 9cc1187 into develop Jul 9, 2026
2 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.

2 participants