Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,14 @@ jobs:
$failed = $false
foreach ($f in $files) {
$script = Get-Content -Raw $f.FullName
$noComments = ($script -split "`n" | ForEach-Object { $_ -replace ';.*$','' }) -join "`n"
$noStrings = $noComments -replace '"[^"\n]*"','' -replace "'[^'\n]*'", ''
$opens = ($noStrings.ToCharArray() | Where-Object { $_ -eq '{' }).Count
$closes = ($noStrings.ToCharArray() | Where-Object { $_ -eq '}' }).Count
# Strings FIRST, then comments. A ';' inside a string literal is not
# a comment -- `Loop Parse EnvGet("PATH"), ";" {` is valid AHK -- and
# stripping comments first truncates that line at the semicolon,
# taking the brace with it and failing correct code (SPEC.md B59).
$noStrings = $script -replace '"[^"\n]*"','' -replace "'[^'\n]*'", ''
$noComments = ($noStrings -split "`n" | ForEach-Object { $_ -replace ';.*$','' }) -join "`n"
$opens = ($noComments.ToCharArray() | Where-Object { $_ -eq '{' }).Count
$closes = ($noComments.ToCharArray() | Where-Object { $_ -eq '}' }).Count
if ($opens -ne $closes) { Write-Host "FAIL $($f.Name): braces $opens / $closes"; $failed = $true }
else { Write-Host "OK $($f.Name): braces $opens / $closes" }
}
Expand All @@ -125,7 +129,7 @@ jobs:
run: |
$ahk = Get-ChildItem -Path "C:\Program Files\AutoHotkey" -Recurse -Filter AutoHotkey64.exe -File -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $ahk) { Write-Host "AutoHotkey64.exe not found; skipping AHK unit tests"; exit 0 }
foreach ($test in @("tests/test_classify_clipboard.ahk", "tests/test_parse_mode.ahk")) {
foreach ($test in @("tests/test_classify_clipboard.ahk", "tests/test_parse_mode.ahk", "tests/test_pythonw_discovery.ahk")) {
$errFile = Join-Path $env:RUNNER_TEMP "_ahk_test.err"
$p = Start-Process -FilePath $ahk.FullName -ArgumentList @('/ErrorStdOut', $test) -RedirectStandardError $errFile -PassThru -Wait
$e = Get-Content -Raw -LiteralPath $errFile -ErrorAction SilentlyContinue
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

## Unreleased

## 2.5.3

**Flowkey survives a Python upgrade.** Removing the interpreter Flowkey's virtual environment was built against broke every hotkey with a modal Windows dialog, and re-running the source installer repaired nothing. Found on a live machine.

### Fixed

- **"Python venv launcher is sorry to say ... did not find executable" on every hotkey.** A virtual environment's `Scripts\pythonw.exe` is not an interpreter — it is a ~250 KB stub that re-execs the interpreter recorded in `pyvenv.cfg`. Upgrading Python 3.13 to 3.14 uninstalls that interpreter but leaves the stub on disk, so the resolver's existence check still passed and every action spawned a dead launcher that hung on a modal dialog. The virtual environment is now accepted only while its base interpreter still exists, verified by reading `pyvenv.cfg` rather than by running the stub — running it to find out is precisely what raised the dialog.
- **Locating Python no longer assumes an install layout.** The rung below the virtual environment was the bare name `pyw.exe`, which the PSF Python Manager installer does not ship at all (it installs `pythonw.exe` under `%LOCALAPPDATA%\Python\bin`), so deleting the stale environment would only have moved the failure. Discovery now walks the PEP 514 registry entries every conformant Windows Python writes, newest 3.11+ first, and resolves `PATH` by hand so the zero-byte Microsoft Store alias stubs that shadow real installs are rejected rather than launched.
- **FastFlowLM reported as "not installed" on a machine where it was installed.** A process only ever sees the environment block built when its session started, and Flowkey normally launches at logon — so FastFlowLM installed (or repaired) afterwards appended itself to the machine `PATH` where Flowkey could never see it. `flm` answered fine from any new shell while `doctor` said `fastflowlm_cli: not found` and every hotkey failed. Provider CLIs are now located by an explicit resolver — `PATH`, then `PATH` as the registry currently holds it, then the known install directories — and `argv[0]` is passed as an absolute path. That last part matters on its own: Windows resolves a bare `argv[0]` against the *parent* process's `PATH`, so repairing the child's environment does not affect the lookup. Detection and execution now share one resolver, so `doctor` can no longer contradict the running app.
- **`flm validate` ran on the raw inherited environment.** It was the only `flm` call site that did not go through `flm_env()`, so it alone missed both the `PATH` repair above and the 2.5.2 `FLM_MODEL_PATH` repair.
- **`install.ps1` repairs an unhealthy virtual environment instead of reporting success.** It carried the same existence-is-health assumption, so re-running the installer on an affected machine printed "venv already present" and fixed nothing. It now checks that the environment's base interpreter exists, rebuilds it when it does not, and finds the interpreter to rebuild with using the same rung order as the app.

## 2.5.2

**The local server starts after a reboot, and start-with-Windows works again.** Both were silent failures with no error anywhere; both were found on a live machine.
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ Flowkey is a Windows desktop assistant that adds local-LLM hotkeys for grammar f

Everything runs locally through [FastFlowLM](https://fastflowlm.com) (AMD Ryzen AI NPU) or, on machines without the NPU, through [Ollama](https://ollama.com) (CPU/GPU) as a secondary provider. No cloud service, analytics, or telemetry is used by the app.

Current version: `2.5.2`
Current version: `2.5.3`

## What's new in 2.5.3

- **Upgrading Python no longer breaks every hotkey.** Flowkey runs its Python helpers from a small virtual environment. When the Python that environment was built against is removed — exactly what a 3.13 to 3.14 upgrade does — the environment keeps a launcher stub that no longer works, and every hotkey stopped with a modal "Python venv launcher is sorry to say..." dialog that had to be dismissed by hand. Flowkey now notices the environment is dead and falls back to a working Python on the machine.
- **Flowkey finds Python however it was installed.** Its only fallback was one specific launcher that the newer Python installer does not ship at all, so a machine with a perfectly good Python could still fail. Flowkey now looks Python up the way Windows itself records it, and ignores the Microsoft Store placeholder that stands in for Python on `PATH` without being it.
- **Flowkey finds FastFlowLM even when Windows hasn't caught up.** If you install FastFlowLM while Flowkey is already running — or Flowkey starts at sign-in before Windows has published the change — FastFlowLM was invisible to Flowkey until you signed out and back in, showing as "not installed" on a machine where it plainly was. Flowkey now looks it up properly instead of trusting what it inherited at startup.
- **Re-running the source installer repairs a broken setup.** It previously reported "venv already present" and changed nothing, because it checked only whether the file was there — not whether it worked.

## What's new in 2.5.2

Expand Down Expand Up @@ -191,4 +198,5 @@ AutoHotkey tests are run by CI on Windows. Locally, run them with AutoHotkey v2:
```powershell
& "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" /ErrorStdOut tests\test_parse_mode.ahk
& "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" /ErrorStdOut tests\test_classify_clipboard.ahk
& "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" /ErrorStdOut tests\test_pythonw_discovery.ahk
```
6 changes: 6 additions & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ Caveman-encoded (compression, not amputation). Paths / ids / action names / numb
- V64: cached update-check result past TTL ⊥ presented as current fact — flagged `stale` (incl. network-failure fallback to disk) ∧ UI labels it ∧ triggers one background forced refresh; ⊥ blocking the tab
- V66: ∀ `flm` child spawned by Flowkey gets `flm_env()`: inherited `FLM_MODEL_PATH` pointing into `config\systemprofile` → replaced w/ `~/.flm`; unset → left unset (⊥ invent a path); sane value → untouched
- V67: autostart Run value MUST name a resolvable exe (absolute-exists ∨ on PATH) — ⊥ bare name we haven't resolved; `get_autostart_state` reports `valid`; daemon startup REPAIRS an enabled-but-unlaunchable entry (repair-only, ⊥ create what the user never enabled)
- V68: python/pythonw discovery ⊥ assume install layout ∨ launcher name. probe order = `GRAMMARFIX_PYTHONW` → `scripts\.venv` (iff `pyvenv.cfg` base ∃) → PEP 514 registry (3.11+, newest, HKCU≻HKLM) → PATH `pyw.exe`/`pythonw.exe`. candidate valid ⟺ ∃ ∧ size>0 (⊥ 0-byte WindowsApps alias stub). venv health = STATIC `pyvenv.cfg` read, ⊥ spawn ∴ a dead venv can never raise its own modal dialog merely to be detected. `install.ps1` ∧ `daemon_client.ahk` share the rung order
- V69: provider CLI (`flm`/`ollama`) argv[0] = ABSOLUTE path via `resolve_cli` (PATH → registry machine+user PATH → known install dirs); ⊥ bare name ∵ Windows resolves argv[0] against the PARENT's PATH ∴ an `env=` PATH repair alone ⊥ suffice. detection (`provider_status`) ∧ execution share ONE resolver ∴ `doctor` ⊥ contradict the app. `flm_env()` repairs PATH ∀ flm child (∧ keeps the B54 `FLM_MODEL_PATH` repair)
- V65: force re-pull of an installed model = provider-correct: FLM `pull` only fetches when ABSENT ∴ force ⇒ remove-then-pull (DESTRUCTIVE on download failure → error says the model is now uninstalled + retry); ollama `pull` already re-fetches on digest change ∴ ⊥ remove. UI confirms before sending force

## §T tasks
Expand Down Expand Up @@ -236,4 +238,8 @@ B54|2026-08-31|THE actual root cause of the "exited early (exit 1)" saga, found
B55|2026-08-31|self-inflicted: 2.5.1 implemented force re-pull as remove-then-pull ∵ I read `flm pull --help | head -20`, which TRUNCATED the option list. `flm --force` exists ("Force re-download even if model exists") ∧ is non-destructive ∴ I shipped a needlessly destructive path + a scary warning|V65; use `flm pull <model> --force`; drop the remove + the "no longer installed" wording; retarget the 2 tests that pinned the destructive contract. Lesson: never conclude a CLI lacks a flag from truncated `--help`
B56|2026-08-31|autostart silently stopped working: `_autostart_command_line()` fell back to the BARE string `"AutoHotkey64.exe"` whenever the installed layout (`APP_DIR\ahk`) was absent — i.e. ∀ dev/source trees, where AHK lives in `vendor\ahk` — ∧ AHK ⊥ on PATH ∴ Windows launched nothing at logon while the Run value still read as "enabled". ⊥ error anywhere|V67; probe `APP_DIR\ahk` → `APP_DIR\vendor\ahk` → `shutil.which`, else return "" (never an unresolved bare name); `get_autostart_state` gains `valid`; daemon repairs an enabled-but-unlaunchable entry at startup (repair-only). Live-verified: entry self-healed to the vendored path, `valid: True`
B52|2026-08-27|dashboard advertised "FastFlowLM v0.9.45 → v0.9.46 available" from a 14-DAY-old cache (real latest 1.0.3): `cache_only` read serves an expired cache verbatim ∧ UI rendered it as current fact; network-failure fallback also served disk while reporting `cached:false` ∧ ⊥ `stale`|V64; mark `stale` on the network-failure fallback (+ carry `checked_at`, `cached:true`); UI labels stale reads "(cached — rechecking…)" ∧ fires ONE background forced refresh so the label self-corrects ⊥ blocking the tab
B57|2026-09-14|live: ∀ hotkey popped a modal `Python venv launcher is sorry to say ... did not find executable at '...\Programs\Python\Python313\pythonw.exe'` ∧ hung there (one stuck `pythonw.exe` per action). cause: a 3.13→3.14 upgrade (PSF Python Manager) uninstalled the base interpreter, leaving `Python313\` a husk (`Lib`/`Scripts`/`share`, ⊥ `python.exe`) — but `scripts\.venv\Scripts\pythonw.exe` survived ∵ it is only a ~250KB STUB that re-execs the path in `pyvenv.cfg`. `ResolvePythonwPath_Impl` accepted it on bare `FileExist` ∴ AHK spawned a dead launcher ∀ time. Worse, the rung below it was the bare name `pyw.exe`, which Python Manager ⊥ ship AT ALL (it installs `pythonw.exe` under `%LOCALAPPDATA%\Python\bin`) ∴ deleting the venv would have failed a 2nd time, differently. `install.ps1` carried the SAME existence-≠-health blind spot (`Test-Path $venvPythonw` → "venv already present") ∴ re-running the installer repaired nothing. Same family as B10 (stale `pytest.exe` shim outliving its interpreter)|V68; portable discovery in BOTH `daemon_client.ahk` ∧ `install.ps1`: static `pyvenv.cfg` base check, PEP 514 registry sweep, 0-byte alias-stub veto, PATH resolved by hand; `install.ps1` rebuilds an unhealthy venv instead of reporting success. New `tests/test_pythonw_discovery.ahk` (12 asserts) pins the dead-venv + alias-stub cases. Live-verified on the broken machine: dead venv rejected ⊥ spawning it, resolved → `pythoncore-3.14-64\pythonw.exe` via registry
B58|2026-09-14|live: FLM 1.0.5 installed, working, ∧ `C:\Program Files\flm` ∈ MACHINE PATH — but the app reported it absent: `doctor` → `fastflowlm_cli: not found`, `flm_validate: flm CLI not in PATH`, ∀ hotkey dead. ∵ a process only ever sees the env block built at SESSION START ∧ Flowkey launches at logon ∴ an FLM installed after that is invisible until sign-out; `shutil.which("flm")` ∧ bare-name argv[0] both read that stale PATH. Nothing was broken except the lookup (`flm version` answered fine from any new shell). Same family as B57 (Python) ∧ B10: trusting an ambient PATH. SELF-CORRECTION mid-fix: the first attempt repaired only `flm_env()["PATH"]` ∧ `provider_status` — `fastflowlm_cli` went green while `model_installed`/`flm_validate` STILL failed, ∵ **Windows resolves argv[0] against the PARENT's PATH ∴ `env=` ⊥ affect that lookup at all**. Caught by re-running live `doctor`, ⊥ by reasoning|V69; `subprocess_util.resolve_exe`/`resolve_cli`; ∀ 9 provider-CLI argv[0] sites absolutised (flm_server serve/list/version, benchmark, provider_runtime pull/remove, pull, first_run, grammar_fix validate, install); `provider_status` shares the resolver; `grammar_fix`'s `flm validate` gains `env=flm_env()` — it was the SOLE flm call site on the raw inherited env ∴ it also silently missed B54. New `tests/test_cli_discovery.py` (11). Live-verified under a stale PATH (`flm` ⊥ resolvable in the launching shell): doctor all-green (`model_installed: yes`, `flm_validate: ready=True`) + a real grammar fix end-to-end
B59|2026-09-14|PR #46 CI red: the AHK brace-balance gate reported `daemon_client.ahk: braces 54 / 55` on code AHK itself parses clean. The gate stripped COMMENTS BEFORE STRINGS (`;.*$` per line, then quoted spans) ∴ `Loop Parse EnvGet("PATH"), ";" {` — a semicolon inside a STRING — was truncated at that `;`, taking the line's `{` with it. A false positive on valid code, ∧ it would fire for any `;` in a literal. Same shape as B48: a brace/comment heuristic mis-reading syntax it doesn't actually parse|swap the order — strings stripped first, then comments. Verified over ∀ 10 `.ahk` files: every pre-existing count UNCHANGED (grammarFix 84/84, tray 34/34, ...) ∧ daemon_client now 55/55. Fixed the gate, ⊥ contorted the source to suit it
B60|2026-09-14|PR #46 automated review, both valid: (1) `ResolvePythonwPath_Impl` cached the resolved interpreter for the whole session w/ ⊥ revalidation ∴ a Python upgrade mid-session pinned the dead path until AHK itself restarted — reintroducing B57 for exactly the long-lived sessions Flowkey has (it autostarts at logon). (2) `install.py` `_has_cmd` was still `shutil.which` ∧ gates ∀ FLM path ∴ it rejected `flm` BEFORE `resolve_cli` could ever run — `postreboot` opened the FLM download page for an already-installed FLM. Detection ⊥ agreeing w/ execution is precisely what V69 forbids, violated in a file I had just edited: I converted the argv ∧ left the guard|V68/V69; cache revalidates on read — file stats ONLY, ⊥ spawn (∴ a dead venv still can't raise its own dialog) — ∧ walks up from `<venv>\Scripts\pythonw.exe` to re-check `pyvenv.cfg`, covering GRAMMARFIX_PYTHONW-supplied venvs too; `_has_cmd` → `resolve_exe`. SELF-CAUGHT while testing: my own AHK fixture wrote a 0-BYTE venv stub ∴ the alias-stub veto fired first ∧ the new dead-venv asserts would have passed for the wrong reason — exposed by the live-venv case failing; fixture now writes content
```
Loading
Loading