Replace hotkey-driven dictate with signal-driven headless mode#219
Merged
Conversation
Recording now starts immediately and runs without a terminal, finishing on SIGTERM (clean exit 0) so a hotkey tool like Hammerspoon can launch `assembly dictate` as a background task and `kill -TERM` / `task:terminate()` to transcribe. SIGINT (Ctrl-C) still cancels (exit 130). The press-Enter/keypress terminal mode and its `core/hotkey.py` (TerminalKeys) backend are removed; dictate now polls a new `signals.stop_on_terminate` latch between mic chunks. Contrast `signals.terminate_as_interrupt` (stream/agent/ speak), which routes SIGTERM into the cancel path instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrDRFsdAYyXwWSudM2da8g
…indow The lock-free config readers and the atomic os.replace in _dump can both lose a race on Windows, which (unlike POSIX) has no atomic replace-over-open: a reader's open or the writer's replace transiently fails with PermissionError while the file is being swapped in. This surfaced as a reliably-red Windows CI on the config-concurrency stress test (test_config_concurrent_writers_*), pre-existing on main since the cross-process write lock landed. Wrap both the read and the replace in a small bounded retry that rides out the sub-millisecond rename window (no-op on POSIX, which never raises here). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrDRFsdAYyXwWSudM2da8g
The filelock-based serialization of config.toml's read-modify-write was a recurring source of Windows CI flakiness, and the lost-update race it closed (two concurrent `assembly` processes clobbering each other's profile/telemetry writes) isn't worth that cost for a single-user CLI. Remove core/config_lock.py, core/locking.py, and the filelock dependency. Writes still go through a load -> mutate -> dump (now config._update) whose _dump does a temp-file + atomic os.replace, so a reader never sees a torn file; writers and readers are otherwise unsynchronized (last write wins). The Windows os.replace sharing-window retry (config._retry_on_sharing_violation) stays, since the lock-free read/replace race it guards is independent of the removed lock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrDRFsdAYyXwWSudM2da8g
…edge the job The community.chocolatey.org download for ffmpeg sometimes hangs for the entire 20-minute job timeout rather than failing fast, so the existing retry loop never got to retry — the stuck `choco install` never returned, and the Windows matrix cell was cancelled (surfacing as a spurious "tests (windows)" failure with the test suite never run). Wrap each attempt in Start-Job + Wait-Job -Timeout so a hung download is killed after 4 minutes and the next attempt actually runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JrDRFsdAYyXwWSudM2da8g
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Replace the interactive hotkey-based dictation interface with a signal-driven headless mode that responds to SIGTERM instead of keyboard input. This enables
assembly dictateto be launched as a background task by external controllers (like Hammerspoon hotkey tools) that send SIGTERM to mean "I'm done dictating."Key changes
Removed
aai_cli/core/hotkey.py: TheTerminalKeysclass that read individual keypresses in cbreak mode is no longer needed. This eliminates the terminal requirement and enables headless operation.Added
stop_on_terminate()context manager inaai_cli/core/signals.py: A new signal handler that latches SIGTERM into a poll-able flag. Unliketerminate_as_interrupt()(which routes SIGTERM to the cancel path), this allows the capture loop to see the flag flip and stop gracefully, transcribing the utterance before exiting cleanly (exit 0). SIGINT (Ctrl-C) remains the cancel path (exit 130).Updated
aai_cli/commands/dictate/_exec.py:TerminalKeyswithstop_on_terminate()_record()to accept astop_requestedcallable instead of aTerminalKeysobjectSTOP_KEYSconstant and the key-polling logicTerminalKeys.__enter__()Updated test harness in
tests/test_dictate_exec.py:FakeKeyswithFakeStopthat yields a boolean predicateFakeStop([True/False, ...])instead ofFakeKeys([key, ...])Updated documentation and help text:
assembly dictatehelp from "Push-to-talk dictation" to "Signal-driven dictation"kill -TERM $(pgrep -f 'assembly dictate')as the stop mechanismImplementation details
The new flow is:
stop_requested()between ~100 ms mic chunksThis design allows
assembly dictateto be used in pipes and as a background task, with external controllers managing the recording lifecycle via signals rather than terminal interaction.https://claude.ai/code/session_01JrDRFsdAYyXwWSudM2da8g