fix(hooks): guard fcntl import on Windows — _shared + session_start#345
Open
Huntehhh wants to merge 2 commits into
Open
fix(hooks): guard fcntl import on Windows — _shared + session_start#345Huntehhh wants to merge 2 commits into
Huntehhh wants to merge 2 commits into
Conversation
truememory/ingest/hooks/_shared.py and session_start.py both had bare `import fcntl` at module top with no `try / except ImportError` guard. fcntl is POSIX-only — on Windows every import raised ModuleNotFoundError before any function ran, which cascaded to every Claude Code hook subprocess that imports from _shared (SessionStart, UserPromptSubmit, Stop). Silent failure mode: no memory extraction after sessions, no context injection at session start, no buffer writes on user prompts. Fix mirrors the established `_HAS_FCNTL` flag pattern already used by ingest/pipeline.py, hooks/core.py, and ingest/hooks/user_prompt_submit.py. The two affected call sites: - _shared.check_extraction_budget — POSIX uses fcntl.flock(LOCK_EX) for atomic read-modify-write across concurrent ingest processes. Windows falls back to non-atomic R/M/W; worst-case race window allows 1-2 extra extractions per hour, acceptable given the alternative is no enforcement at all. - session_start._scan_stale_sessions — POSIX uses a non-blocking advisory lock to prevent concurrent scans. Windows skips the lock; the backlog drainer's atomic .json → .processing rename already deduplicates any overlapping work. Tests in tests/ingest/test_hooks_windows_portability.py (8 tests) pin the _HAS_FCNTL flag contract on both modules, verify check_extraction_budget behaves correctly without fcntl (allows, enforces cap, resets hourly), and include a POSIX-only regression lock that fcntl.flock(LOCK_EX) is still acquired on platforms where it's available — so a future refactor can't silently drop cross-process coordination. Co-Authored-By: claude-opus-4-7 <wontreply@getfucked.ai>
Documents the bug (every Claude Code hook crashing on Windows), the fix (_HAS_FCNTL guard pattern matching the rest of the codebase), and the test coverage (8 regression tests). Co-Authored-By: claude-opus-4-7 <wontreply@getfucked.ai>
This was referenced May 17, 2026
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.
Summary
truememory/ingest/hooks/_shared.pyandtruememory/ingest/hooks/session_start.pyboth called bare
import fcntlat module top with notry / except ImportErrorguard.
fcntlis POSIX-only — on Windows every import raisedModuleNotFoundErrorbefore any function ran. That cascaded to every ClaudeCode hook subprocess that imports from
_shared(SessionStart,UserPromptSubmit, Stop), silently breaking the hook-based memory-extraction
pipeline on Windows.
Fix mirrors the established
_HAS_FCNTLflag pattern already used byingest/pipeline.py,hooks/core.py, andingest/hooks/user_prompt_submit.py.Both call sites (
_shared.check_extraction_budgetandsession_start._scan_stale_sessions) now gate theirfcntl.flockcalls on_HAS_FCNTL. POSIX behavior is unchanged; Windows falls through to thenon-locking path with documented worst-case race semantics (1-2 extra
extractions per hour for the budget check, deduplicated overlap for the stale
scan).
This is a sibling fix to the WNOHANG guard in the cold-start resilience PR
(if that one is open) — same POSIX-only-API class of bug, same
hasattr/try-except-guard pattern.Changes
truememory/ingest/hooks/_shared.pyimport fcntl→try / except ImportError+_HAS_FCNTLflag;check_extraction_budgetguardsfcntl.flock(LOCK_EX)on the flag; documented fallback semanticstruememory/ingest/hooks/session_start.pyimport fcntl→ same_HAS_FCNTLguard;_scan_stale_sessionsguardsfcntl.flock(LOCK_EX | LOCK_NB)on the flag with inline comment explaining the dedup pathtests/ingest/test_hooks_windows_portability.py_HAS_FCNTLflag contract on both modules,check_extraction_budgetbehaves correctly withoutfcntl(allows, enforces cap, resets hourly), POSIX-only regression lock thatfcntl.flock(LOCK_EX)is still acquired on platforms where it's availableCHANGELOG.mdTest Plan
python -m pytest tests/ingest/test_hooks_windows_portability.py -v→ 7 passed + 1 skipped on Windows (the POSIX regression lock); 8 passed on POSIXpython -c "from truememory.ingest.hooks import _shared, session_start, user_prompt_submit; print('OK')"→ exits 0ModuleNotFoundError: No module named 'fcntl'in stderr — the hook should run to completion and inject memory contextpython -c "from truememory.ingest.hooks._shared import check_extraction_budget; print(check_extraction_budget())"and confirm the budget counter increments correctly (i.e. the lock is still doing its job)Design Notes
named mutex? The Windows-specific Mutex API (
win32event.CreateMutex/pywin32) would add a non-stdlib dependency for a code path whoseworst-case bug (1-2 extra extractions per hour, never a corruption) is
far below the threshold that justifies a new dependency.
filesystem-marker file? The orphaned-session scan only runs every 15
minutes per session-start; even if two scans run concurrently, the
backlog drainer's atomic
.json → .processingrename deduplicates anyoverlapping work. Adding a marker-file lock would be more code with
similar race characteristics.
same observable behavior across platforms wherever the trade-off is
acceptable. Disabling functionality on Windows would create a
silent-feature-gap that's harder to debug than a documented race window.
Co-Authored-By: claude-opus-4-7 wontreply@getfucked.ai
Merge ordering
Status:
MERGEABLE(clean againstorigin/main).Blocks: #351 (Windows subprocess portability —
session_start.py:~226Popen edit needs this PR's_HAS_FCNTLguard at file top to be in place first).Depends on: none. Disjoint from #344 / #346 / #347 / #348 / #349 / #350 / #352 / #353.
Recommended sequence position: between #344 and #346.
Full sequence (10 PRs from a 3-agent coordination):