Two Files Instead of a Socket
If you were wiring a Python engine to a Swift helper, the reflex would be an HTTP server in the helper, or a socket, or XPC. Oriel uses two JSON files in a folder under its data root:
desired.json, written by the engine: which inputs to open, in which mode and bitrate, into which folder, plus one-shot requests (peeks and gimbal moves), each answered once by its id.status.json, written by the recorder about once a second: its heartbeat, the camera authorization, every video device it can see with the modes each offers, and the state of each open input (frames, measured fps, the open segment, errors).
The recorder reads desired.json and makes the world match it. The engine reads status.json and believes it only while the heartbeat is fresh. This is the desired state pattern you may know from orchestration systems, shrunk to one Mac and two files.
What a File Buys
The module's docstring lists the reasons in one sentence: a file channel needs no port, no socket permission and no Local Network grant, survives either side restarting, and is readable by a human. Each of those mattered.
- No port, no grant. On recent macOS, a launchd agent that dials a network address can be silently refused by the Local Network privacy gate (track seven meets it from the hands' side). A file on a local disk is outside that gate entirely.
- Restarts are boring. If the engine restarts in the middle of a session, the recorder keeps recording, because what it was asked to do is still on disk. If the recorder restarts, it reads the same file and reopens the same inputs. Neither side has to replay a connection's history.
- A human can read it. When something is wrong,
cat status.jsontells you what the recorder sees and thinks, with no client to write first.
The Three Ways a File Channel Fails
Files are not free. Oriel met all three classic failures, and each one is now a rule in code.
1. Two writers lose an edit. Every change to desired.json is read, edit, write. On 2026-11-03 two camera parks were handed over at the same second; both read the file, both wrote it back, and one camera's move never reached the recorder. Now every edit holds a lock for the whole read-edit-write: a lock per path for threads inside the engine, and flock on a sibling .lock file for other processes, because the command-line tools write the same file. The house sign repeats it as a rule: every desired.json edit holds recorder_link.editing.
2. A failed read becomes an empty write. A read that fails (a file mid-replacement, a disk error) is easy to treat as "nothing asked". Write that back and the recorder stops every recording on its next pass. So a read that is about to be written back is strict: if the file is there and the disk read fails, it raises and nothing is changed. A file that holds no JSON at all still reads as nothing asked in the live code; the atomic replace is what keeps a half-written file from ever being seen. The miniature below is stricter and raises on undecodable JSON too. On the recorder's side the mirror rule holds: a desired.json it cannot decode keeps the last one it read and reports desired_error, where it once stopped every recording.
3. A stale request wakes up. A peek or a move left in the file by an engine that died mid-wait would be answered by the next recorder process, opening and turning a camera that nobody is waiting for, possibly after the gate closed. So the engine clears every one-shot request as it starts, when by definition nobody can be waiting for one.
A Heartbeat That Means Something
status.json is only evidence while it is fresh. If its heartbeat is more than five seconds old, the engine reports the recorder offline and every look refuses with that reason; it never answers from the last thing a dead recorder said. Installers depend on the same heartbeat, and it taught one more lesson: a stopping recorder writes one last status on its way out, so an installer that waited for "a heartbeat after I restarted it" in whole seconds once took build 188's last word for build 189's first. Installers now compare against a sub-second timestamp taken after the old recorder is gone.