Skip to content

v0.3.2 — Windows watch support, and a doctor that tells the truth

Choose a tag to compare

@riponcm riponcm released this 07 Sep 08:04
· 49 commits to main since this release

Windows works properly now — and pjm doctor stops saying green when it cannot see.

pip install -U projectmem

Every fix below was found by running the thing on Windows, not by reading the code.

The watcher works on Windows

pjm watch --daemon crashed there with AttributeError: module 'os' has no attribute 'fork'. Daemonisation used the POSIX-only os.fork() and os.setsid(); it now spawns a detached worker with subprocess.Popen — DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW on Windows, start_new_session=True elsewhere.

Contributed by @medium-effort in #13, who also filed a precise reproduction in #12. Thank you.

Fixing that crash uncovered a second bug one function away, hidden behind it. _running_pid() checked liveness with os.kill(pid, 0) — the POSIX idiom, which does not port: on Windows os.kill routes to TerminateProcess, so signal 0 is not a liveness check at all and fails whether or not the process exists. The consequences, all observed:

  • pjm watch --status always reported "not running"
  • the PID file was deleted as stale while the worker ran on
  • pjm watch --stop found nothing to stop
  • every further --daemon slipped past the already-running guard and leaked another worker — two runs left four orphans

Liveness now uses OpenProcess + GetExitCodeProcess on Windows. Access-denied counts as alive: a process you cannot open still exists, and calling it dead is precisely what orphans it.

A doctor that does not report success when it cannot look

Three separate ways pjm doctor printed a clean bill of health while something was wrong. They share a shape, and it is the worst one a check can have — it does not merely miss the problem, it reports the problem solved, which ends the investigation.

A Store-installed Claude Desktop was invisible. A Microsoft Store (MSIX) install runs in an AppContainer, where the app's writes to %APPDATA% are redirected into the package's LocalCache. Inside the container the config still appears at %APPDATA%\Claude — a genuine view, not a fake one — but from any ordinary process it does not exist there at all. Doctor found nothing and said everything was fine while two servers pinned with --root were running. Store installs are now discovered under %LOCALAPPDATA%\Packages\Claude_* and labelled Claude Desktop (Store), because the file lives at a path most users have never seen.

An unreadable config counted as a clean one. Permissions, a lock, a broken link — all were skipped silently, which made "I could not look" identical to "I looked and it is fine". They now get their own warning.

A fix undone by the client looked like flakiness. Remove --root, watch doctor go green, and the running client can rewrite the whole file from the copy it loaded at startup and restore the pin. Doctor now remembers what it saw last time and says so: "Claude Desktop was clean on 2026-09-07 06:29 and is pinned again." It also tells you to quit the client before editing, since advice that silently fails while the app is open is not advice.

Client config paths now read %APPDATA% and %LOCALAPPDATA% rather than assuming they sit under the home directory — the old assumption breaks on any redirected profile. And the project you are standing in is checked too: a per-repo .mcp.json pins just as effectively as a global config.

--at src/foo.py counted for nothing

Every place that turned an event location into a file path used location.split(":")[0], with two faults in one line.

A bare path has no colon, so pjm score's file-coverage component ignored it — up to 20 points lost in silence, and weaker churn detection in pjm precheck — even though --at's own help text invites that form. And a Windows path carries a drive colon, so C:\src\foo.py:42 resolved to a phantom file called C.

Both fixed in one shared parser now used by score, precheck, staleness, brief, context and export. --at src/foo.py and --at src/foo.py:42 finally count the same, and a ClassName.method location is still correctly not a file.

Also

  • Global memory honours $PROJECTMEM_HOME. global_dir() read a constant fixed at import from Path.home(), while the registry resolved the variable per call. Since append_event() auto-promotes library mentions into the global store, anything running under an isolated home still wrote invented gotchas into the real ~/.projectmem/global/.
  • Two tests only ever passed on POSIX — one asserted a raw path where the product correctly emits JSON-escaped backslashes, the other set $HOME, which Windows ignores in favour of %USERPROFILE%. All nineteen $HOME fixtures now go through one portable helper.

Upgrading

Nothing to do beyond pip install -U projectmem. No config changes, no schema changes, no new dependencies. Worth running pjm doctor once afterwards — on Windows especially, it may now report a pinned config it could not see before.

Full detail: CHANGELOG.md