fix(observer): ocr_enabled flag — bypass screencapture popup storm#10
Merged
Conversation
User report (2026-05-02): after merging Step 5 and starting codec-observer,
the screencapture-based OCR fired the macOS Screen Recording permission
popup repeatedly — "over a hundred times" per user. Each click of "Allow"
on the popup grants only one-time access; the persistent grant is the
toggle in System Settings → Privacy & Security → Screen & System Audio
Recording.
Compounding bug in codec_observer._get_screenshot_ocr:
with ThreadPoolExecutor(max_workers=1) as ex:
future = ex.submit(_screencapture_and_ocr_blocking)
return future.result(timeout=timeout_s)
The `with` exit calls executor.shutdown(wait=True) which BLOCKS until
the thread finishes. When the popup appears, screencapture's subprocess
hangs waiting for the user, and the thread can't finish. Result: my
"100ms timeout" actually waited up to 5 seconds (full subprocess.run
timeout chain on screencapture+osascript), and BOTH the first attempt
AND the Q5.1 retry trigger their own popup. Per poll: ~2 popups +
5s blocking until user dismisses.
Hotfix: add `ocr_enabled` config flag (default True — preserves Step 5
design behavior on permission-granted installs) that, when set false,
skips _get_screenshot_ocr entirely. Buffer still captures active_window
+ clipboard + recent_files; only OCR is bypassed. No screencapture call,
no popup risk.
~/.codec/config.json:
"observer": {
"ocr_enabled": false ← user can flip to disable OCR per-machine
}
3 new tests in tests/test_observer.py:
- test_ocr_enabled_false_skips_screenshot_call (sentinel-based: raises
if _get_screenshot_ocr is called when flag is false)
- test_ocr_enabled_default_is_true (preserves existing behavior)
- test_ocr_enabled_true_calls_screenshot (sanity check)
Result: 33/33 tests passing in 0.21s. Zero side effects.
Permanent fix for OCR users: grant Screen Recording AND Accessibility to
both `node` (PM2 parent) and `python3.13` (codec-observer process) in
System Settings → Privacy & Security. Then the toggle stays on across
restarts.
Deeper subprocess-leak issue (ThreadPoolExecutor.with-exit blocks on
shutdown wait=True) is NOT fixed in this commit — would require restructuring
the OCR call to use Popen with explicit kill on timeout, or
ThreadPoolExecutor.shutdown(wait=False, cancel_futures=True) (Python 3.9+).
Tracked for follow-up. The ocr_enabled flag is the safe immediate fix.
This was referenced May 2, 2026
AVADSA25
pushed a commit
that referenced
this pull request
May 2, 2026
…(Steps 5/6/7) Phase 2 (Observer + Triggers + Shift Report) merged and production-stable: - Step 5 (PR #9 824a52f) + hotfix PR #10 (26e6add) — `observer.ocr_enabled` flag - Step 6 (PR #11 2d2ff3f) — Trigger System (matcher + cooldown + consent) - Step 7 (PR #12 0e40687) — end-of-day shift report Net: +91 passing tests (823/20/73), 0 new failures, 0 new skips. Live audit proof captured: shift_report_started+_completed paired emits at 2026-05-02T18:49:40Z with shared cid=5f188e5485e5.
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
User report (2026-05-02 morning): after merging Step 5 and starting
codec-observer, the macOS Screen Recording permission popup fired "over a hundred times" — the user clicked Allow but it kept coming back.Root cause
Two-part bug in
_get_screenshot_ocr:macOS Allow is one-shot. Persistent grant requires the System Settings toggle, not the popup's Allow button. Clicking Allow on the popup only authorizes that single call.
ThreadPoolExecutor.__exit__blocks on shutdown. When the popup appears, screencapture's subprocess hangs waiting for the user. My code:__exit__callsshutdown(wait=True)— blocks until the thread finishes. The thread can't finish until the popup is dismissed. The "100ms timeout" actually waited ~5s (subprocess.run's own 2s+3s chain). And the Q5.1 retry triggered a SECOND popup. Per poll: ~2 popups + 5s blocking.Fix
Add
ocr_enabledconfig flag (defaultTrue— preserves Step 5 behavior on permission-granted installs). Whenfalse,poll()skips_get_screenshot_ocrentirely. Buffer still capturesactive_window+ clipboard + recent_files; only OCR is bypassed.Test plan
tests/test_observer.py:test_ocr_enabled_false_skips_screenshot_call— sentinel-based, raises if_get_screenshot_ocris called despite the flagtest_ocr_enabled_default_is_true— preserves existing behaviortest_ocr_enabled_true_calls_screenshot— sanity check for the default pathpytest tests/test_observer.py→ 33/33 passing in 0.21s. Zero side effects (Apple Reminders=0, /tmp/codec_*.txt=0).Deployment
This PR ships the code change. The user's runtime config has ALREADY been edited to
ocr_enabled: false(atomic write, backup at~/.codec/config.json.bak-1777736987). After merge:Observer resumes polling without OCR. No popups possible.
Permanent fix (out of scope for this hotfix)
Two follow-ups for a future PR:
nodeANDpython3.13via System Settings. Persistent grant survives restarts.ThreadPoolExecutor + future.result(timeout=...)withsubprocess.Popen+ explicitproc.kill()on timeout. The current shutdown-wait pattern is the underlying root cause; theocr_enabledflag is just the immediate off-switch.🤖 Generated with Claude Code