Skip to content

Support Windows: portable advisory locking and itimer-free hook timeout - #54

Open
volkangulecx wants to merge 1 commit into
tigerless-labs:mainfrom
volkangulecx:windows-support
Open

volkangulecx wants to merge 1 commit into
tigerless-labs:mainfrom
volkangulecx:windows-support

Conversation

@volkangulecx

Copy link
Copy Markdown

What

agent-memory fails to import and run on Windows today, although the README lists Windows as a supported platform. Two POSIX-only dependencies are loaded at import/run time:

  • fcntl (used for flock advisory locking in core/locking.py and core/observation.py) — raises ModuleNotFoundError at import, so the entire package fails to load.
  • signal.setitimer / signal.SIGALRM (the hook timeout watchdog in adapters/hook_entry.py) — raises AttributeError when mem-hook fires.

This PR makes both portable without changing POSIX behavior.

How

  • New core/portlock.py: an exclusive advisory lock that delegates to fcntl.flock on POSIX and msvcrt.locking on Windows, surfacing contention as BlockingIOError on both platforms so call sites keep catching one exception type. locking.py and observation.py call it instead of importing fcntl directly.
  • hook_entry._arm / _disarm degrade to no-ops where signal.setitimer / SIGALRM are unavailable (Windows). The host's own hook timeout still bounds the call.

Invariants preserved: files stay the single source of truth, the lock stays advisory and per-store, and POSIX uses flock exactly as before.

Tests

  • tests/unit/test_portlock.py — lock round-trips (blocking and non-blocking), accepts a file object or a raw descriptor, preserves the descriptor offset, and the lock-holding modules import on every platform (the Windows regression).
  • tests/unit/test_hook_timeout_guard.py — the watchdog is a no-op without itimer and round-trips with it.

ruff check, mypy, and the existing test_locking.py pass. Verified end to end on Windows 11: mem init / record / recall / rebuild, and the Claude Code SessionStart hook injecting MEMORY.md.

Out of scope / known follow-up

On Windows, tests/system/test_read_observability.py::test_host_transcript_limits_and_failure still fails: it asserts a 0o600 file mode, which POSIX permission bits do not express on Windows. That is independent of locking and signals and is left for a separate change.

agent-memory lists Windows as a supported platform but fails to import and
run there. Two POSIX-only dependencies are loaded at import/run time:

- fcntl (flock advisory locking in core/locking.py and core/observation.py)
  raises ModuleNotFoundError at import, so the whole package fails to load.
- signal.setitimer / signal.SIGALRM (the hook timeout watchdog in
  adapters/hook_entry.py) raises AttributeError when mem-hook fires.

Add core/portlock.py: an exclusive advisory lock that delegates to
fcntl.flock on POSIX and msvcrt.locking on Windows, surfacing contention as
BlockingIOError on both so call sites keep catching one exception type.
locking.py and observation.py call it instead of fcntl directly. Make
hook_entry._arm/_disarm no-ops where itimer signals are unavailable; the
host's own hook timeout still bounds the call.

POSIX behavior is unchanged: the lock is still advisory and per-store and
still uses flock. Files remain the single source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Apageoflove

Copy link
Copy Markdown

Ran this on real Windows 11 (Python 3.13.5). Everything below is measured on this machine, not inferred, except where marked. The one thing I did not test is a long-lived contention storm, only bounded acquisition(长时间高争用场景我没实测过).

The six new tests: five pass, test_arm_disarm_round_trip_with_itimer skips on win32 as designed.

Full tests/unit/ against a comparison baseline of main run with an fcntl shim (main cannot even import on Windows without one): main gives 471 passed / 30 failed, this branch gives 480 passed / 26 failed. The failure-set diff is strictly positive: zero tests that fail here but pass on main, and four hook/muse tests that fail on main now pass (the itimer no-op paths doing their job).

Cross-process exclusion, the property store_lock actually needs: parent holds the lock, a spawned child's non-blocking acquire gets BlockingIOError; after unlock, the child acquires. Verified.

Two non-blocking notes. _arm on Windows is a no-op, so mem-hook loses its timeout guard entirely there rather than getting a replacement — the PR title's "itimer-free hook timeout" reads like an alternative mechanism exists, worth one sentence in the description saying the guard is disabled on Windows (and maybe a TODO for a thread-based timer later; this reading of the title is 仅个人判断). And 26 unit tests still fail on Windows for reasons this PR does not touch: scoped-recall path separators (forward-slash scopes never match backslash paths), hook setup writing POSIX-style absolute commands, a couple of timestamp cases. All of them fail identically on the main baseline, so they are pre-existing gaps, not regressions — but a follow-up would keep "Windows supported" honest end to end.

Net: this unblocks importing and using the store on Windows outside those edges, with no regression I could find. Filing the follow-up issue with the full 26-test list next.

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