4 fps was not "live", and it was a number I picked rather than measured. The engine actually produces ~12 distinct frames a second, so most of it was being left on the floor. Now: poll a little ahead of the engine and drop frames identical to the last one by hash. Measured end to end - 131 frames in 10 s, 13.1 fps, 20.3 KB each, 259 KB/s, zero duplicates. Every byte on the wire is a picture the viewer has not seen, and the rate follows the camera instead of a constant. Also records why this is MJPEG rather than passing the camera's own compressed video through, which would be smoother, cheaper and use no CPU. Probed the office camera: main 2304x1296@15, sub 800x448@15 - and BOTH are H.265, despite stream paths ending in ".264". Browsers play H.264 everywhere and H.265 only on some platforms, so passthrough cannot rely on it, and transcoding HEVC on the shop PC would put a video encoder on the machine already doing the recognition. So probe_source now reports `codec`. It decides what is possible, an installer can usually change it, and otherwise the only way to learn it is to read RTSP by hand - which is how this was found. The RTSP libraries used to establish that are NOT kept: they were only ever imported by a spike test, and two large dependencies in a shipped binary to answer a question OpenCV already knows is a bad trade. Their `go get` had also silently bumped the agent to go 1.25 and broken the desktop build, which is its own argument. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
Behavision agent (Go)
The half of the edge install that touches the network. The Python engine keeps the cameras and the models; this keeps the tray icon, the UI shell, the MQTT connection and the offline queue.
┌─ agent (Go) ───────────────────┐ ┌─ engine (Python) ────────┐
│ tray icon + WebView2 window │ │ RTSP capture │
│ supervises the engine process │───────▶│ YuNet / ArcFace / FAISS │
│ MQTT publish + offline spool │◀───────│ SQLite (biometric) │
│ S3 handoff, tenant config │ local │ localhost API + events │
└────────────────────────────────┘ HTTP └──────────────────────────┘
Why the split
Go cannot run ONNX, OpenCV or FAISS, so the engine stays Python and ships frozen. Go is here for what it is actually better at: a durable queue that survives a store's internet dropping, a supervised child process, and one language shared with the server so the MQTT contract has a single definition.
Why not a Windows service
A service runs in session 0 and cannot draw a tray icon — that is Windows session isolation, not a library limitation. Since the product is "user starts and stops it from the tray", the agent is a normal user-session process that spawns the engine as a child. That also means it never needs elevation at runtime: starting a child process does not, controlling a service does.
internal/engine is written so a service wrapper can be added later without
touching the supervision logic.
Layout
main.go entry point, mode dispatch
internal/engine start/stop/supervise the Python engine, health polling
internal/spool durable event queue (survives restart and outage)
internal/mqtt broker client, publishes from the spool
internal/config tenant identity, broker settings, credentials
frontend/ React UI served into the WebView
Build
go build ./... # agent alone
wails build # agent + frontend, once the UI is added