Nothing ever set the child's working directory, so it took the parent's - and an app started by double-clicking its bundle is handed "/", not anywhere useful. On macOS the symptom was `python: No module named behavision` repeating forever, because the dev engine is invoked as `-m behavision` and that resolves against the working directory. The same app launched from a terminal inside the repo worked perfectly, which is exactly the shape of a bug that survives every test a developer runs. It only appeared when the app was started the way a user starts one. Config.EngineDir, empty meaning the install root, set by both launchers - the desktop app and the headless agent, which had identical code and the identical omission. It matters beyond this case: the shipped Windows engine is a one-folder PyInstaller build whose relative paths should resolve beside itself rather than beside Explorer's idea of a current directory. Verified by double-clicking the bundle with nothing in the environment: engine up on 8010 (401, gated), w600k_r50 on CoreML, gallery 5/5 embeddings usable and none stranded, both office cameras connected and streaming, and head office reporting cameras 2/2 one heartbeat later. Two things that showed up while proving it, both the product being honest rather than faults: - The camera at .121 was genuinely unreachable for several minutes, and last_error said so in words an installer can act on - "cannot reach 192.168.1.121:554 - No route" - rather than `connected: false`. That field was added yesterday for precisely this. - Head office briefly showed cameras 0/1 against a local 2/2. That is a 60-second heartbeat, not a disagreement; the next one read 2/2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
Behavision desktop
The store-facing app: a tray icon, a window, and the supervisor for the Python recognition engine.
Why one process, not three
The tray, the window and the supervisor all need the same state, and a user who quits the tray expects recognition to stop. Splitting them means two things can disagree about whether the engine is running.
It is deliberately 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. Spawning a child process also needs no elevation, while controlling a service does, so this design never triggers UAC at runtime.
Layout
main.go wails.Run, window options
app.go the methods bound to the frontend
tray.go fyne.io/systray — wails v2 has no tray of its own
icons.go tray icons generated at run time, not embedded
internal/local client for the engine on 127.0.0.1:8010
internal/cloud client for https://mcp.loyaly.ai
frontend/ React + Vite
The supervisor, durable spool, broker client and path resolution come from
../agent/pkg/* — the same tested code the headless agent runs, imported
rather than copied.
Build
cd frontend && npm install && npm run build # then, from this directory:
wails build -platform windows/amd64
wails build needs the Wails CLI:
go install github.com/wailsapp/wails/v2/cmd/wails@v2.9.2
Without it, go build still type-checks everything provided
frontend/dist exists — the embed directive requires it.
What the frontend talks to
Nothing is imported from generated bindings. src/bridge.js calls
window.go.main.App.* directly, so npm run build works without running
wails generate, and there is one place that handles "the engine is not
running yet" — the state every screen has to survive on a fresh install.