majormonitoring-gapverifiedfirsthand
A receipt-OCR watcher crashed 11 times on a missing module and nobody was told
A launchd job that OCRs receipt images dropped into a folder and adds them to a household ledger had crashed with `ModuleNotFoundError` 11 times in a row. No alert ever fired, and the images just sat there unprocessed.
Cause
The OCR dependency, pyobjc (Quartz/Vision), was never declared in the requirements file. The Python it had been installed into during development was not the Python the launchd job definition actually called, so the job broke silently the moment the interpreter changed.
Consequence
Automatic receipt import was down for more than a day. The ledger itself was not corrupted; its SHA-256 matched before and after.
Fix
Declare the dependency, install it into the exact interpreter launchd calls, and verify the import works in a stripped-down environment. The job now runs under a monitoring kernel whose preflight tries import Quartz, Vision before doing anything else.
What happened
While building a small kernel to monitor
launchd jobs and migrating the existing jobs onto it, the last exit code of
each job got checked one by one. The receipt watcher was the only one sitting
at exit=1.
The job wakes up when a receipt image lands in a watched folder, runs macOS Vision OCR on it, and appends a line to a household ledger. Its log held the same traceback eleven times over:
ModuleNotFoundError: No module named 'Quartz'
The last failure was the evening before it was discovered. Up to that point, not a single dropped image had been imported.
The chaos on the ground
The nasty part is that this job only fires when a person drops a file in. A job that runs hourly makes noise when it goes quiet. A job that fires rarely looks exactly the same broken as it does on a day with no receipts. It failed eleven times, and all eleven failures went nowhere.
Worse, the development notes said “pyobjc installed.” That was true — for one
Python, at one point in time. The launchd job definition called
/usr/local/bin/python3.14, and pyobjc was not in it.
Same shape as the translation job that failed 13 runs in a row unnoticed for
five days: python3 resolved to something different under launchd than it did
in an interactive shell.
Root cause
The real cause was that the dependency was declared nowhere. The requirements file listed only openpyxl and pytest; the pyobjc packages the OCR module needed (Quartz/Vision/Cocoa) were missing. With nothing written down, there was no way to know what went missing when the interpreter changed underneath the job. And a job that dies on a missing dependency keeps dying quietly unless something is watching it.
The fix
- Added
pyobjc-framework-Vision>=12.0to the requirements file, with a comment explaining why. - Installed it with
pip install --userinto/usr/local/bin/python3.14, the interpreter launchd actually calls, without touching the framework’s own site-packages. - Confirmed the import works under a minimal
env -i HOME=$HOMEenvironment, closing the gap between launchd and the interactive shell. - Moved the job onto the monitoring kernel with
FAIL_THRESHOLD=2. For a job that fires this rarely, waiting for three failures would delay detection by days. - A preflight step now tries
import Quartz, Visionbefore the job runs. If the modules are missing, the run is recorded as a failure before it ever reaches OCR.
The image that had been stuck in the folder turned out to be a screenshot, and the gate added after an earlier incident correctly rejected it. The ledger’s SHA-256 matched before and after: nothing was corrupted.
Sources
- Firsthand account from the site operator. There is no public write-up to link to.